Agent 面试通关 / 02
工具管理:参数校验、工具路由与百级工具库
工具调用是 Agent 区别于普通对话模型的核心能力。面试官在这个维度考的不是“你知不知道 function calling”,而是“模型不听话怎么办”和“工具多了怎么管”——这两个问题决定了你的 Agent 能不能上生产。
Q:工具描述写得再好,模型也瞎传参数怎么办?
来源:腾讯 Agent 岗终面
新手答:“加强 Prompt,让模型输出 JSON。”
高手答:
三层防线:
- Schema 里写“负面描述”:比如“城市名参数,请直接使用用户输入文本,切勿从地址中自行解析提取”。告诉模型不该做什么,比告诉它该做什么更有效。
- 输出后做硬校验:用 Pydantic 模型校验,类型不对、枚举值不匹配的直接拒掉,让模型重试。
- 关键参数业务兜底:比如查询订单的工具,模型传回的
order_id,必须先调用一次“订单是否存在”的校验接口。绝不让未经核实的 ID 直接下发给数据库。
差距在哪:新手的答案停留在 Prompt 层面——这是最弱的防线,模型不一定听。高手的三层防线是递进的:Prompt 引导 → 格式硬校验 → 业务逻辑兜底。即使模型完全不听话,后两层也能拦住。面试官考的是“你有没有做过需要防御性编程的系统”。
Q:你们工具库有上百个工具,怎么让模型快速选对?
来源:腾讯 Agent 岗终面
新手答:“把所有工具描述发给模型让它选。”
高手答:
不可能全发,上下文不够,且会干扰。我们做了个轻量级工具路由层:
- 用户请求来时,先用 FastText 之类的小模型快速提取意图关键词
- 用关键词去工具向量库做语义检索,召回 top 5
- 最关键的一步:用一个精调过的 7B 小模型,给这 5 个工具的相关性做精排序,只把 top 2 工具的完整 schema 交给大模型做最终调用
这个流程让工具选择准确率从 ~70% 提到了 95%+。
差距在哪:新手的方案在工具超过 20 个时就会崩——上下文被工具描述占满,模型反而选不好。高手的回答展示了一个完整的召回-排序管线(和搜索引擎的架构一样),每一层都有明确的职责。面试官考的是“你能不能把 Agent 的子问题抽象成一个工程问题来解决”。
Q:多工具场景下的调度策略?
来源:阿里 AI Agent 开发一面
新手答:“模型自己选。”
高手答:
多工具场景的核心问题是选哪个、按什么顺序、并行还是串行。
- 意图路由前置:用户请求先经过轻量分类器或规则,判断需要哪类工具,不让大模型在全量工具里大海捞针
- 依赖分析决定编排:分析工具之间的输入输出依赖关系——无依赖的并行执行,有依赖的串行编排,减少总耗时
- 优先级与降级:多个工具都能用时,按可靠性、延迟、成本排优先级;主工具超时自动降级到备选
- 结果合并与冲突处理:多工具返回结果可能矛盾,需要合并策略——取最新的、取置信度最高的、或让模型做最终判断
调度策略不是模型能力问题,是工程编排问题——模型负责决策“做什么”,编排层负责“怎么执行”。
差距在哪:新手把调度交给模型“自己想办法”。高手的回答把多工具调度拆成四个环节:路由、编排、降级、合并——每个环节都有明确策略。面试官考的是你能不能把多工具协作从“模型自己选”变成一个可控的工程流程。
Q:Mock 是怎么实现的?在自动化生成测试的场景下
来源:抖音基础架构 Agent 一面
新手答:“用 unittest.mock 替换依赖。”
高手答:
Mock 的实现分三步:识别依赖 → 生成 Mock 对象 → 注入测试。
第一步:识别需要 Mock 的依赖
从 AST 和 import 分析中提取外部调用——数据库连接、HTTP 请求、文件 I/O、第三方 SDK 调用。判断标准是:这个依赖在测试环境里能不能正常运行。能运行的不 Mock,不能运行的才 Mock。
第二步:生成 Mock 对象
根据依赖的接口签名生成 Mock:
- 函数调用:Mock 返回值,类型和实际返回一致
- 类实例:Mock 整个类,保留方法签名,返回值用合理的默认值
- 异步调用:生成
AsyncMock,保持await语义
# 自动生成的 Mock 示例
@patch('module.db_client.query')
def test_get_user(mock_query):
mock_query.return_value = {"id": 1, "name": "test"}
result = get_user(1)
assert result["name"] == "test"
mock_query.assert_called_once_with(user_id=1)
第三步:注入策略
不同语言注入方式不同——Python 用 @patch 装饰器或 with 语句,Java 用依赖注入框架(Mockito),JavaScript 用 jest.mock。关键是 Mock 的粒度:Mock 太多会让测试脱离真实行为,Mock 太少会因为环境依赖跑不起来。
差距在哪:新手只知道用 Mock 库替换依赖。高手的回答展示了自动化场景下的完整 Mock 生成流程——先识别、再生成、再注入——且点出了 Mock 粒度的核心取舍。面试官考的不是你会不会用 Mock 库,而是你能不能让 Agent 自动决定“Mock 什么、怎么 Mock”。
Q:如果工具调用是成功的,但返回结果语义不完整,模型很容易误判,你怎么设计中间层?
来源:腾讯大模型应用开发二面
新手答:“让模型自己判断返回值是否完整。”
高手答:
这个问题非常常见。很多工具从接口层面看是 200 成功,但业务语义上其实不够用——比如只返回了一个 code 没有返回解释信息,或者字段含义不清,模型会自行脑补。
解决方式是加一个 tool adapter 或 semantic wrapper,把原始结果转成统一、可解释的中间表示:
- 字段补全:缺失字段用默认值或“未知”显式标注,而不是让模型猜
- 错误翻译:把错误码转成自然语言描述,比如
code: 404→"未找到对应记录" - 单位归一:统一数据格式,比如日期统一成 ISO 格式、金额统一带币种
- 空值处理和置信度标注:区分“查了没有”和“没查到”,标注数据可信度
核心原则:不要把外部 API 的脏数据直接回喂给模型。中间层先清洗,让模型看到的是“可推理对象”,而不是原始接口垃圾。
差距在哪:新手让模型自己判断——模型没有业务语义知识,判断不了。高手在工具和模型之间加了一个 semantic wrapper 层,做字段补全、错误翻译、单位归一和置信度标注。面试官考的是你有没有意识到“工具调用成功 ≠ 结果可用”,以及怎么在中间层做数据治理。
Q:工具多导致 token 数过多,怎么解决?
来源:字节 Agent 实习二面
新手答:“减少工具数量。”
高手答:
砍工具是下策——工具是 Agent 的能力,砍多了能力就残了。核心思路是让模型在每次调用时只看到需要的工具,而不是全量工具。
1. 动态工具加载:
- 不把所有工具的 schema 一次性塞进 system prompt
- 根据用户当前意图,只加载相关工具。比如用户在问天气,就只加载天气/地理类工具,不加载数据库/文件操作类工具
- 意图识别可以用轻量分类器或关键词规则,成本极低
2. 工具描述压缩:
- 很多工具描述写得太冗长——把 200 字的描述压缩到 50 字以内,只保留“这个工具做什么”和“关键参数”
- 参数的详细约束放在二级描述中,模型选中工具后再加载完整 schema
3. 分层工具组织:
第一层:工具大类(5-8 个)
→ 信息查询类 / 数据操作类 / 文件处理类 / 通信类 / ...
第二层:具体工具(每类 5-10 个)
→ 天气查询 / 股票查询 / 地图搜索 / ...
模型先选大类,再在大类下选具体工具。两次选择的 token 开销远小于一次展示全量工具。
4. 工具向量检索(已有工具百级以上时):
- 用户 query 做 embedding,和工具描述做语义匹配,只召回 top 3-5 的工具
- 这就是把“工具选择”变成了一个“检索”问题
差距在哪:新手的“减少工具”是削足适履。高手用动态加载、描述压缩、分层组织、语义检索四个方法,在不砍工具的前提下把 token 开销控制住。面试官考的是你面对 token 预算约束时的工程化解题能力。
Q:MCP Server 是怎么构建的?
来源:字节 Agent 实习二面
新手答:“就是写个 API 接口。”
高手答:
MCP(Model Context Protocol)是 Anthropic 提出的模型与外部工具/数据源的标准化通信协议,目标是让 Agent 以统一的方式调用不同工具,不需要为每个工具写专门的适配代码。
MCP Server 的核心职责是:把外部能力(API、数据库、文件系统等)封装成符合 MCP 协议的标准工具,供 Agent 调用。
构建一个 MCP Server 的关键步骤:
- 定义 Tool Schema:
- 每个工具需要声明名称、描述、输入参数(JSON Schema)和输出格式
- 描述要写给模型看——清晰说明“什么时候该用这个工具”和“参数怎么填”
- 实现 Handler:
- 每个工具对应一个 handler 函数,接收标准化的参数,执行实际操作,返回标准化的结果
- Handler 内部做参数校验、错误处理、超时控制
- 选择传输方式:
- stdio:通过标准输入输出通信,适合本地工具(如文件操作、命令行工具)
- HTTP/SSE:通过 HTTP 请求 + Server-Sent Events 通信,适合远程服务
- 本地开发用 stdio 简单快速,生产部署用 HTTP/SSE 做服务化
- 注册与发现:
- Client 端(Agent)通过配置或服务发现机制找到 MCP Server
- Server 启动时声明自己提供的工具列表,Client 按需选择
和直接写 API 的区别:
- 普通 API:每个工具一套接口定义、一套调用方式、一套错误处理
- MCP:所有工具统一协议,Agent 不需要知道底层是 REST 还是 gRPC 还是本地调用——只需要知道 tool name 和参数
差距在哪:新手把 MCP 等同于“写 API”。高手理解 MCP 是一个协议层抽象——统一了工具发现、参数定义、调用方式和错误处理,让 Agent 能以即插即用的方式扩展能力。面试官考的是你对 Agent 工具生态标准化趋势的理解。
Q:大厂开源的 CLI 工具(如 lark-cli)和 MCP 有什么区别?它们跟直接调 API 又有什么不同?
来源:大厂 Agent 面试高频题
新手答:“CLI 就是命令行工具,MCP 就是协议,API 就是接口,三个不同的东西。”
高手答:
这三者解决的问题不同,服务的“用户”也不同:
1. API——给程序用的原始接口
API 是最底层的能力暴露方式。比如飞书开放平台的 REST API,你要自己处理鉴权、拼参数、解析响应、处理错误码。它的“用户”是开发者写的代码。
开发者代码 → HTTP 请求 → 飞书 API → 响应 JSON
2. CLI 工具——大厂跟进 Agent 生态的抢位之作
关键背景:lark-cli、coze-cli 这批大厂 CLI 工具集中涌现,不是偶然——它们是 MCP 和 Agent 生态爆火之后,各平台为了抢占 Agent 工具生态入口而快速推出的。
CLI 本质上是 machine-friendly 的接口形态。和 GUI(人类友好)不同,CLI 天然适合被程序和 Agent 调用——结构化的输入输出、可脚本化、可管道组合。大厂推 CLI 而不是只提供 API,是因为 CLI 降低了 Agent 接入的门槛:不用写 SDK 集成代码,直接 lark-cli send --chat "xxx" --text "hello" 就能调用。
Agent / 脚本 → lark-cli send --chat "xxx" --text "hello"
↓ 内部封装鉴权、参数、错误处理
飞书 API 调用 → 结构化输出
但 CLI 的局限很明显:每个平台一套 CLI,Agent 需要为每个平台学一套命令。
3. MCP——Agent 工具调用的统一协议
MCP 解决的是 CLI 解决不了的问题:标准化。它不是封装某一个平台的 API,而是定义了 Agent 发现和调用任意工具的统一协议。不管底层是飞书 API、数据库还是本地文件系统,Agent 只需要按 MCP 协议交互。
Agent → MCP 协议 → MCP Server A(封装飞书能力)
→ MCP Server B(封装数据库)
→ MCP Server C(封装本地文件系统)
MCP 的价值在于:Agent 不需要知道底层是 REST、gRPC 还是 CLI——一套协议打通所有工具。
那大厂为什么不直接做 MCP Server,还要出 CLI?
因为 CLI 是更低成本的试水方式:不需要实现完整的 MCP 协议栈,先用 CLI 把平台能力暴露给 Agent 生态,抢个身位。而且 CLI 可以被 MCP Server 二次封装——社区已经有大量“CLI → MCP Server”的适配层了。
核心区别总结:
| 维度 | API | CLI 工具 | MCP |
|---|---|---|---|
| 服务对象 | 程序代码 | Agent / 脚本(machine-friendly) | AI Agent(协议级) |
| 设计动机 | 开放平台能力 | 抢占 Agent 工具生态入口 | 统一 Agent 工具标准 |
| 抽象层级 | 原始接口 | 平台级封装 | 协议级抽象 |
| 覆盖范围 | 单一平台 | 单一平台 | 跨平台统一 |
| 工具发现 | 查文档 | --help |
协议内置 tools/list |
| 典型场景 | 后端集成 | Agent 快速接入单一平台 | Agent 统一工具管理 |
它们的关系是递进的:API 是原始能力 → CLI 是大厂面向 Agent 生态的快速封装 → MCP 是最终的协议层标准。一个 MCP Server 内部可以调 API,也可以调 CLI,它们不互斥。
差距在哪:新手把三者当成并列的“不同的东西”。高手看到的是 Agent 工具生态的演进脉络——API 一直在,CLI 是大厂看到 MCP/Agent 趋势后的抢位动作,MCP 是最终统一标准。面试官考的是你能不能看到这波 CLI 扎堆出现背后的产业逻辑,以及理解为什么 Agent 时代的终局是协议层标准化而不是各家出各家的 CLI。
Q:大模型的 Function Call 是什么?Tool Use 一般怎么用?
来源:蚂蚁集团智能体与大模型应用一面
新手答:“就是让模型调用函数。”
高手答:
Function Call(Tool Use)是让大模型不只生成文本,还能结构化地调用外部工具的核心机制。
工作原理:
sequenceDiagram
participant U as 用户
participant M as 大模型
participant T as 工具/API
U->>M: "帮我查北京明天天气"
M->>M: 分析意图,决定调用 weather_query 工具
M-->>T: {"name": "weather_query", "arguments": {"city": "北京", "date": "明天"}}
T-->>M: {"temp": "22°C", "weather": "晴"}
M->>U: "北京明天晴,气温 22°C,适合出行"
模型不是直接执行代码——它输出的是结构化的调用意图(工具名 + 参数 JSON),由外部编排层执行实际调用,再把结果回传给模型做最终整合。
和普通 Prompt 的关键区别:
| 维度 | 普通 Prompt | Function Call |
|---|---|---|
| 输出 | 自由文本 | 结构化 JSON(工具名 + 参数) |
| 能力边界 | 只能用训练数据 | 可接入实时数据和外部系统 |
| 可控性 | 低(自由发挥) | 高(Schema 约束参数类型和取值) |
| 典型场景 | 问答、创作 | 查天气、操作数据库、发消息、执行代码 |
在 Agent 项目中的典型 Function Call:
- 信息查询类:知识库检索、数据库查询、API 调用——Agent 不靠记忆回答,而是实时查
- 操作执行类:创建工单、发送通知、修改配置——Agent 不只回答问题,还能执行动作
- 代码执行类:运行 Python 代码做计算、执行 SQL 查询——把模型的“推理”变成“验证”
工程上的关键设计:
- Schema 定义要精准:工具描述写给模型看,参数 Schema 约束类型和枚举值。描述越精确,模型选错工具和瞎传参的概率越低
- 返回值要结构化:工具返回不要是一大段文本,而是 JSON——模型解析结构化数据比理解自然语言更稳定
- 错误处理要前置:工具调用可能失败,返回值里必须带状态码和错误信息,让模型能根据错误类型决定下一步
差距在哪:新手把 Function Call 等同于“调函数”。高手理解它是模型从“文本生成器”升级为“行动执行器”的关键机制,且清楚 Schema 设计、返回值规范和错误处理这些工程细节决定了 Tool Use 的稳定性。面试官考的是你对 Agent 核心能力的理解深度。
Q:MCP 和 Skills 的本质区别是什么?都是工具调用,为什么需要两套机制?
来源:蚂蚁集团智能体与大模型应用二面
新手答:“MCP 是协议,Skills 是能力,不太一样。”
高手答:
MCP 和 Skills 解决的是不同层次的问题,虽然都和“Agent 如何使用工具”相关,但它们不在同一个抽象层级上:
MCP(Model Context Protocol)——工具调用的“通信协议”
MCP 解决的是:Agent 如何发现、调用、获取结果。它定义了一套标准的交互格式——工具怎么注册(tools/list)、参数怎么传(JSON Schema)、结果怎么返回。类比网络协议:MCP 是 HTTP,定义了请求/响应的格式,不关心你用这个请求做什么。
Skills——任务执行的“能力单元”
Skills 解决的是:Agent 面对某类任务时,怎么思考、用什么工具、按什么流程执行。一个 Skill 包含触发条件、专用 Prompt、可用工具集、输出约束。类比:Skill 是一个“微型 Agent 配置”,告诉 Agent“遇到这类问题时按这个方案办”。
flowchart TB
subgraph skill["Skill(能力单元)"]
direction TB
S1["触发条件:用户说'整理面经'"]
S2["专用 Prompt:分类+格式化指令"]
S3["工具集:文件读写、Grep 搜索"]
S4["输出约束:Markdown 格式"]
end
subgraph mcp["MCP(通信协议)"]
direction TB
M1["工具发现:tools/list"]
M2["参数规范:JSON Schema"]
M3["调用方式:stdio / HTTP"]
M4["结果格式:标准化响应"]
end
skill -->|"Skill 内部通过 MCP 调用工具"| mcp
核心区别对照:
| 维度 | MCP | Skills |
|---|---|---|
| 抽象层级 | 通信协议层 | 业务能力层 |
| 解决的问题 | “怎么调工具” | “什么场景用什么方案” |
| 包含内容 | 工具描述、参数规范、传输方式 | 触发条件 + Prompt + 工具集 + 输出约束 |
| 类比 | HTTP 协议 | Web 应用的一个 Controller |
| 复用粒度 | 单个工具 | 一套完整方案 |
为什么需要两套机制:
只有 MCP 没有 Skills → Agent 知道怎么调工具,但不知道什么时候该调哪个,需要每次都靠模型自己推理,不稳定。
只有 Skills 没有 MCP → Agent 知道该怎么办,但每个工具要单独写适配代码,不可扩展。
两者结合:Skills 定义“策略”,MCP 提供“基础设施”。Skill 里的工具调用通过 MCP 协议完成——这样新增工具只需要写 MCP Server,不需要改 Skill 逻辑;新增能力只需要写 Skill,不需要改工具接口。
差距在哪:新手把 MCP 和 Skills 当成两个并列的概念。高手看到的是分层架构——MCP 在协议层解决“怎么调”,Skills 在业务层解决“怎么用”,两者是上下层关系而非替代关系。面试官考的是你能不能把 Agent 架构按层次拆清楚。
Q:Function Calling 的本质价值是什么?它解决的是“模型能力问题”还是“系统约束问题”?
来源:Agent 开发面试 30 题
新手答:“让模型能调工具,解决的是能力问题。”
高手答:
Function Calling 解决的主要是系统约束问题,不是模型能力问题。
没有 Function Calling 的时候,模型也能“调工具”——你在 Prompt 里告诉模型“如果需要查天气,请输出 {"tool": "weather", "city": "北京"}“,模型大概率能输出正确的 JSON。但这个方案有三个致命问题:
- 输出格式不可靠:模型可能在 JSON 前面加一句“好的,我来查一下”,或者少一个引号,导致解析失败
- 调用时机不可控:你不知道模型这次输出是“要调工具”还是“在正常回复”,必须靠正则或关键词猜
- 参数类型无约束:price 字段可能传成字符串“一百”,而不是数字 100
Function Calling 的本质价值是把“模型想调工具”这件事从自由文本变成了结构化协议:
没有 FC:模型生成文本 → 你用正则猜它要不要调工具 → 手动解析参数 → 祈祷格式正确
有了 FC:模型明确返回 tool_call 结构 → 系统确定性地知道要调工具 → 参数有 schema 约束
这不是让模型“更聪明”了,而是给模型和系统之间建立了一个明确的通信协议——模型的意图(调什么工具、传什么参数)不再是需要“猜”的自由文本,而是有格式保证的结构化消息。
差距在哪:新手觉得 FC 是一种“新能力”。高手理解 FC 本质是模型和系统之间的接口协议——解决的是“怎么可靠地把模型意图传递给系统”这个工程问题。面试官考的是你对 FC 的理解停留在“会用”还是“理解设计动机”。
Q:你会如何设计工具 schema,才能降低模型传错参数、漏参数、乱调用的问题?
来源:Agent 开发面试 30 题
新手答:“写清楚参数说明就行。”
高手答:
工具 schema 设计的核心原则是降低模型的决策负担——参数越少、约束越紧、描述越具体,出错概率越低。
1. 参数设计——越少越好,越受限越好:
- 合并冗余参数:不要同时暴露
start_date和start_timestamp,选一种格式,内部转换 - 用枚举代替自由文本:
sort_by不要让模型自由填,用enum: ["price_asc", "price_desc", "rating"] - 必填和选填分清楚:
required字段必须标明,可选参数给合理的default值,减少模型需要做的决定
2. 描述设计——说“别做什么”比“该做什么”更重要:
{
"name": "search_hotel",
"parameters": {
"city": {
"type": "string",
"description": "城市名,直接使用用户原文(如'北京'),不要从地址中自行提取城市"
},
"check_in": {
"type": "string",
"description": "入住日期,格式 YYYY-MM-DD。如果用户说'下周五',请转换为具体日期"
}
}
}
3. 命名设计——名字要自解释:
get_weather比fetch_data好——名字越具体,模型越不容易误调- 相似工具要通过命名区分清楚:
search_hotel_by_cityvssearch_hotel_by_id,而不是一个search_hotel靠参数区分
4. 工具粒度——一个工具做一件事:
一个“大而全”的工具(既能搜索、又能预订、还能取消)会让模型在参数选择上出错。拆成 search_hotel、book_hotel、cancel_booking 三个工具,每个的参数空间小而确定。
差距在哪:新手觉得“描述写清楚”就够了。高手从参数设计、描述技巧、命名规范、工具粒度四个角度系统性地降低出错率。面试官考的是你有没有做过“把模型对接到真实工具”的工程经验——只有踩过坑的人才知道 schema 设计有这么多讲究。
Q:同一个能力是做成“一个大而全工具”还是“多个小工具”,怎么权衡?
来源:Agent 开发面试 30 题
新手答:“拆小一点好,职责单一。”
高手答:
不能一刀切。拆不拆、怎么拆,取决于模型的工具选择负担和参数填充难度:
| 维度 | 大而全工具 | 多个小工具 |
|---|---|---|
| 模型选择负担 | 低(只需选一个工具) | 高(需从多个里选对的) |
| 参数复杂度 | 高(参数多、组合多) | 低(每个工具参数少) |
| 错误定位 | 难(哪步出错不好查) | 易(哪个工具失败一目了然) |
| 编排灵活性 | 低(绑定了固定流程) | 高(可以自由组合) |
什么时候用大工具:
- 操作有强事务性——查库存 + 锁库存 + 扣款必须原子执行,拆开会有一致性风险
- 用户感知是一个动作——“帮我订机票”不应该让用户看到拆成了五步
什么时候拆小工具:
- 子步骤可以独立使用——搜索酒店和预订酒店是两个独立需求
- 需要灵活编排——“先搜再比再订”的顺序可能因用户需求变化
- 参数空间差异大——搜索只需要城市和日期,预订还需要用户信息和支付方式
实际工程中的折中方案:对外给模型暴露小工具(降低选择和参数负担),对内用编排层把小工具组合成事务(保证一致性)。模型只需要说“我要订这个酒店”,编排层自动执行“校验 → 锁房 → 扣款 → 确认”的事务流程。
差距在哪:新手只记住了”职责单一”的教条。高手从模型负担、参数复杂度、事务性、编排灵活性四个维度做权衡,且给出了”对外小工具 + 对内事务编排”的折中方案。面试官考的是你在工具设计时有没有系统性的权衡框架。
Q:手撕一个 ReAct 架构的 Agent,实现文件操作(找文件、删除文件)
来源:AI 工程师面试(手撕代码,可借助 AI)
新手答:写了个 while 循环拼 Prompt,没有工具抽象,逻辑和 I/O 混在一起。
高手答:
这道题考的是能不能用 ReAct 范式把”工具定义 → Agent 编排 → 多轮交互”串起来。用 LangGraph 的 create_react_agent 实现最清晰:
第一步:定义工具集
每个文件操作封装成独立工具,用 @tool 装饰器标注名称和描述(模型通过描述选择工具):
import os
import glob
from langchain_core.tools import tool
@tool
def list_py_files(directory: str) -> list[str]:
“””列出指定目录下所有 .py 文件的路径”””
return glob.glob(os.path.join(directory, “**/*.py”), recursive=True)
@tool
def check_has_main(file_path: str) -> bool:
“””检查指定 Python 文件中是否包含 main 函数定义”””
with open(file_path, “r”) as f:
content = f.read()
return “def main” in content or 'if __name__' in content
@tool
def count_lines(file_path: str) -> int:
“””统计指定文件的行数”””
with open(file_path, “r”) as f:
return len(f.readlines())
@tool
def delete_file(file_path: str) -> str:
“””删除指定文件,返回操作结果”””
os.remove(file_path)
return f”已删除: {file_path}”
第二步:创建 ReAct Agent
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model=”gpt-4o”)
tools = [list_py_files, check_has_main, count_lines, delete_file]
agent = create_react_agent(llm, tools)
create_react_agent 内部实现的就是 ReAct 循环:思考(选工具 + 定参数)→ 执行(调工具)→ 观察(读返回)→ 思考 → …,直到模型认为任务完成。
第三步:执行任务
# 任务 1:找包含 main 的 py 文件
result = agent.invoke({
“messages”: [{“role”: “user”, “content”: “找出 /workspace 目录下所有包含 main 函数的 .py 文件”}]
}, config={“configurable”: {“thread_id”: “task-1”}})
# 任务 2:删除行数最短的 py 文件
result = agent.invoke({
“messages”: [{“role”: “user”, “content”: “找出 /workspace 下行数最短的 .py 文件并删除它”}]
}, config={“configurable”: {“thread_id”: “task-2”}})
Agent 的实际执行链路(任务 2 为例):
flowchart TB
A[“思考:要找行数最短的 py 文件\n先列出所有 py 文件”] --> B[“行动:list_py_files('/workspace')”]
B --> C[“观察:返回 5 个文件路径”]
C --> D[“思考:逐个统计行数”]
D --> E[“行动:count_lines 逐个调用”]
E --> F[“观察:a.py=10, b.py=3, c.py=25...”]
F --> G[“思考:b.py 行数最短,删除它”]
G --> H[“行动:delete_file('b.py')”]
H --> I[“观察:已删除”]
I --> J[“思考:任务完成,输出结果”]
面试官可能追问的细节:
- 为什么要用
@tool装饰器而不是直接写函数:@tool会自动从类型注解和 docstring 生成 JSON Schema,这是模型做 function calling 时需要的工具描述 - thread_id 的作用:支持多轮对话,同一个 thread_id 下的对话共享记忆,Agent 能引用之前的执行结果
- 如果要加安全限制怎么办:对
delete_file加权限检查——限制只能删除特定目录下的文件、删除前要求用户确认(interrupt_before) - 和直接写 Python 脚本的区别:脚本是写死的流程。ReAct Agent 能根据中间结果动态调整——比如文件读不出来时自动换一种方式查找,不需要提前枚举所有异常路径
差距在哪:新手写一堆 if-else 硬编码。高手用标准的 ReAct 框架——工具定义(@tool)、Agent 创建(create_react_agent)、任务执行(invoke),代码简洁且可扩展。面试官考的是你能不能用 Agent 范式(而非脚本思维)解决问题,且理解 ReAct 循环的执行机制。
这类题的答题模式
工具管理题的核心是防御性思维:
1. 不信任模型的输出——总会有瞎传参数的时候
2. 分层防御——Prompt 引导 + 格式校验 + 业务兜底
3. 大规模工具管理用检索思路——召回 + 排序,不要全塞给模型
4. 每一层都要有明确的兜底策略
面试官听到“加强 Prompt”就知道你只在 Demo 层面做过。听到 Pydantic 校验、业务接口校验、工具向量库检索,才会觉得你做过真实系统。
下一篇建议继续看: