工具设计与实现
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 为例):
思考:要找行数最短的 py 文件,先列出所有 py 文件
↓
行动:list_py_files('/workspace')
↓
观察:返回 5 个文件路径
↓
思考:逐个统计行数
↓
行动:count_lines 逐个调用
↓
观察:a.py=10, b.py=3, c.py=25...
↓
思考:b.py 行数最短,删除它
↓
行动:delete_file('b.py')
↓
观察:已删除
↓
思考:任务完成,输出结果面试官可能追问的细节:
- 为什么要用
@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 循环的执行机制。
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 题
新手答:“拆小一点好,职责单一。”
高手答:
不能一刀切。拆不拆、怎么拆,取决于模型的工具选择负担和参数填充难度:
| 维度 | 大而全工具 | 多个小工具 |
|---|---|---|
| 模型选择负担 | 低(只需选一个工具) | 高(需从多个里选对的) |
| 参数复杂度 | 高(参数多、组合多) | 低(每个工具参数少) |
| 错误定位 | 难(哪步出错不好查) | 易(哪个工具失败一目了然) |
| 编排灵活性 | 低(绑定了固定流程) | 高(可以自由组合) |
什么时候用大工具:
- 操作有强事务性——查库存 + 锁库存 + 扣款必须原子执行,拆开会有一致性风险
- 用户感知是一个动作——“帮我订机票”不应该让用户看到拆成了五步
什么时候拆小工具:
- 子步骤可以独立使用——搜索酒店和预订酒店是两个独立需求
- 需要灵活编排——“先搜再比再订”的顺序可能因用户需求变化
- 参数空间差异大——搜索只需要城市和日期,预订还需要用户信息和支付方式
实际工程中的折中方案:对外给模型暴露小工具(降低选择和参数负担),对内用编排层把小工具组合成事务(保证一致性)。模型只需要说“我要订这个酒店”,编排层自动执行“校验 → 锁房 → 扣款 → 确认”的事务流程。
差距在哪:新手只记住了“职责单一”的教条。高手从模型负担、参数复杂度、事务性、编排灵活性四个维度做权衡,且给出了“对外小工具 + 对内事务编排”的折中方案。面试官考的是你在工具设计时有没有系统性的权衡框架。