代码语言

知识点思维导图

29 个知识节点

Python(27) - Agent 与工具调用

读完后,你应能完成以下任务:

  • 绘制“Python(27) - Agent 与工具调用 / 零、本篇在阶段五里的位置”的关键对象与数据流,解释“如果 23 篇的「messages 是你自己维护的对话历史」还没读透,建议回去补一下——本篇 Agent 的「记忆」「多步」全靠操作这个数组实现。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Python(27) - Agent 与工具调用 / 先建立前端锚点:tool use ≈ 给模型一张「函数注册表」”设计正常与异常输入,验证“tool use 就是把这张表交给大模型:你告诉模型「我这有 getWeather 和 searchDoc 两个函数,各自要什么参数」,模型在需要时不直接执行(它执行不了),而是回你一句:「请帮我调 getWeather('北京')」。”,输出首个偏差位置与回归测试结果。
  • 实现“Python(27) - Agent 与工具调用 / 把结果喂回去:一轮完整的工具调用闭环”的最小代码或配置,检验“拿到模型的「点菜单」后,流程是:① 真正执行函数;”,输出命令、结果与 Diff,并说明不适用边界。

前面 23~26 篇里,模型一直是个「关在盒子里的聪明人」——你问它答,它博学但不能动手:不能查实时天气、不能读你数据库、不能算一道它没把握的数学题。本篇解决的就是「让模型能动手」这件事。核心是三个概念:tool use(工具调用)——给模型注册一组它能调的函数;ReAct——让模型在「思考 → 调工具 → 看结果 → 再思考」之间循环;多步推理——这个循环带着记忆跑下去,直到把任务做完。一句话,Agent ≈ 一个 while 循环,循环体里模型反复调用你注册的函数,像你写前端时反复 await fetch(...) 再根据结果决定下一步。

一、零、本篇在阶段五里的位置

这是阶段五(AI 编程)依赖链的终点,前四篇全是它的地基:

主题 给本篇提供了什么
23 调用大模型 API 发请求、收回复、messages 数组(Agent 的记忆就是这个数组
24 Prompt 工程 function calling 的概念(≈ 函数注册表)、结构化输出
25 embedding 把文字变坐标(Agent 检索类工具会用到)
26 RAG 检索 + 拼接 + 生成(RAG 本质是「只调一次检索工具」的最简 Agent)
27(本篇) Agent 与工具调用 把上面全串起来:注册工具 + 循环调用 + 多步推理

如果 23 篇的「messages 是你自己维护的对话历史」还没读透,建议回去补一下——本篇 Agent 的「记忆」「多步」全靠操作这个数组实现。


二、先建立前端锚点:tool use ≈ 给模型一张「函数注册表」

你在前端写过「事件 → 回调」「命令 → 处理函数」这种映射吧?比如一个命令面板:

// JavaScript:一张「命令名 → 处理函数」的注册表,典型的前端模式
const handlers = {
  getWeather: (city) => fetch(`/api/weather?city=${city}`).then(r => r.json()),
  searchDoc: (kw) => fetch(`/api/search?q=${kw}`).then(r => r.json()),
}
// 来了个命令名,就去表里找对应函数来调
const result = await handlers[cmd.name](./28-Agent与工具调用/...cmd.args)

tool use 就是把这张表交给大模型:你告诉模型「我这有 getWeathersearchDoc 两个函数,各自要什么参数」,模型在需要时不直接执行(它执行不了),而是回你一句:「请帮我调 getWeather('北京')」。真正的执行还是你的代码干,执行完把结果再喂回给模型。

你(代码)                          模型
─────────                          ─────
1. 注册工具表 + 用户问题  ───────►  
                                   2. "我需要 getWeather('北京')"
3. 真正执行 getWeather('北京')  ◄───  (模型只会"点菜",不下厨)
   拿到 {temp: 12, ...}
4. 把结果喂回去  ───────────────►  
                                   5. "北京现在12度,记得加件外套"

类比 → 边界:类比——注册表的形态、「按名字找函数」的思路,和前端命令模式一模一样。边界——关键差异是「谁决定调哪个」。前端是你写死的 handlers[cmd.name],调度逻辑在你代码里;这里是模型自己决定调哪个、传什么参数、甚至调不调。你交出去的不只是函数表,还有「调度权」。这是 Agent 和普通程序最本质的不同:控制流由模型动态决定,不是你预先写死的 if/else


三、tool use 实操:注册一个工具,让模型来点菜

接着第 24 篇的 function calling 往下走,看完整代码。OpenAI 的 tools 参数要求你用 JSON Schema 描述每个函数(描述「形状」,回忆第 24 篇 Pydantic 转 schema 那套):

发起请求,把工具表一起带上:

模型不会直接回答天气,而是回一个调用请求,藏在 message.tool_calls 里:

易踩坑:call.function.arguments字符串,不是 dict,要 json.loads 才能用——这点和第 24 篇「模型返回 JSON 字符串」一脉相承。模型负责「填表」,类型转换是你的事。


四、把结果喂回去:一轮完整的工具调用闭环

拿到模型的「点菜单」后,流程是:① 真正执行函数;② 把执行结果作为一条 role: "tool" 的消息塞回 messages;③ 再请求一次,模型这次就能基于结果给出最终答案。

回头看 messages 现在长这样,你就明白「记忆」是怎么回事了——它就是一个不断变长的数组:

[
  {role: "user",      content: "北京现在穿外套合适吗?"},   ← 用户问题
  {role: "assistant", tool_calls: [get_weather("北京")]},  ← 模型点菜(无文本,只有调用)
  {role: "tool",      content: '{"city":"北京","temp_c":12}'}, ← 我们回传的执行结果
  ... 下一步模型基于以上全部内容继续 ...
]

类比 → 边界:类比——这跟你前端「发请求 → 拿响应 → 把响应塞进 state → 重新渲染」的单向数据流神似,messages 数组就是那个 state。边界——前端 state 你想怎么改都行;这里 messages顺序和配对是有硬约束的:带 tool_calls 的 assistant 消息后面,必须紧跟对应 tool_call_id 的 tool 消息,缺了或乱序模型 API 会直接报错。这是新手最常见的坑之一。


五、ReAct:把单轮闭环变成「带记忆的循环」

上面只调了一轮工具。但真实任务往往要调多次:「查北京天气,再查上海天气,比较一下哪边更冷」——模型得调两次 get_weather,看完两个结果才能比较。

ReAct(Reason + Act,推理 + 行动)就是这件事的范式:让模型在一个循环里反复「思考下一步 → 行动(调工具)→ 观察结果 → 再思考」,直到它认为任务完成、不再点菜为止。

// JavaScript 伪代码:你其实写过这种"轮询到满足条件才停"的循环
let state = initial
while (!done(state)) {        // 没完成就继续
  const action = decideNext(state)   // 想下一步干啥
  const obs = await doAction(action)  // 真去做,拿结果
  state = update(state, obs)          // 把结果并进状态
}
return state

Agent 主循环和它结构完全一样,只是「想下一步」交给了模型,「做」是执行工具:

把上面的循环画出来,就是 Agent 的全貌:

        ┌──────────────────────────────────────────┐
        │                                          │
        ▼                                          │
   ┌─────────┐   想调工具?   ┌──────────┐   把结果   │
   │ 模型思考 │ ────yes────► │ 执行工具  │ ──喂回去──┘
   └─────────┘              └──────────┘
        │ no(不再点菜)
        ▼
   返回最终答案

类比 → 边界:类比——这就是个 while 循环,「带记忆」指的是 messages 一直不清空、越滚越长,模型每轮都能看到全部历史。边界——和前端 while 有两个要命的差异:① 每一圈都是一次付费 API 请求,循环 5 圈就是 5 次调用,token 还在累积变贵,所以 max_steps 不是可选项而是必需的安全阀(前端死循环顶多卡浏览器,这里死循环是烧钱);② 循环能不能停由模型决定,不是你的确定性条件,模型可能「想不通」一直点菜,max_steps 就是你强行拉闸的开关。


六、多步推理:循环跑起来后到底发生了什么

「多步推理」不是什么新机制,它就是第四节那个循环实际转了多圈的结果。拿「比较北京和上海哪边更冷」举例,循环内部的真实轨迹:

第 1 圈:模型思考"要比较,得先有两地温度" → 点菜 get_weather("北京")
        我们执行 → 喂回 {temp_c: 12}
第 2 圈:模型看到北京12度 → "还差上海" → 点菜 get_weather("上海")
        我们执行 → 喂回 {temp_c: 18}
第 3 圈:模型看到两个结果都齐了 → 不再点菜,直接答
        "北京12度、上海18度,北京更冷。" → finish_reason=stop,循环退出

关键体会:模型不是一开始就规划好全部步骤,而是走一步看一步——每拿到一个观察结果,才决定下一步干啥。这正是「Reason + Act」交替的含义,也是它能处理事先不知道要几步的任务的原因。

对照前端:很像你写的「分页拉取直到没有下一页」——你事先不知道要拉几次,每次拿到响应看 hasMore 再决定要不要继续。区别是这里的「要不要继续」是模型基于语义判断的,比 hasMore 这种布尔标志灵活得多,但也更不可控。

6.1 几个工程上必须知道的点

  1. 上下文会膨胀:每圈都往 messages 里塞东西,转得越多数组越大,token 成本越高、还可能撞上模型上下文长度上限。长任务要考虑裁剪历史或做摘要。

  2. 工具会失败get_weather 调真实 API 会超时、报错。别让异常炸穿循环,而是把错误信息当作一种观察结果喂回给模型,让它自己决定重试还是换路子:

  3. 别盲目信任模型点的菜:模型可能传非法参数、调本不该调的工具。涉及写数据库、发消息、花钱的「危险工具」,执行前要做校验甚至人工确认(这点和你前端「危险操作弹二次确认框」一个道理)。


七、不用裸写循环:框架与现成方案

上面手写循环是为了讲清原理。真实项目里你通常会用现成框架,它们把循环、工具注册、错误处理、历史管理都封好了——就像前端你不会裸写 XMLHttpRequest,而是用 axios / React Query

你想要的 前端类比 Python 生态常见选择
封装好的 Agent 循环 React Query 帮你管请求状态 LangChain、LlamaIndex 的 Agent
把 Python 函数自动转成工具 schema 装饰器自动注册路由 框架的 @tool 装饰器(少写手搓 JSON Schema)
多 Agent 协作编排 状态机 / 工作流引擎 LangGraph 等

先吃透手写版再上框架——否则框架报错时你会完全不知道发生了什么。本篇的循环就是所有框架内部的核心,框架只是替你把它和一堆周边能力打包了。

一句话边界:框架降低的是「写样板代码」的成本,不降低你对「模型无状态、记忆靠 messages、循环要有上限、工具会失败」这些本质的理解需求。这些是地基,框架是楼。


八、总结

  • 零、本篇在阶段五里的位置:| 23 | 调用大模型 API | 发请求、收回复、messages 数组(Agent 的记忆就是这个数组) |
  • 先建立前端锚点:tool use ≈ 给模型一张「函数注册表」:tool use 就是把这张表交给大模型:你告诉模型「我这有 getWeather 和 searchDoc 两个函数,各自要什么参数」,模型在需要时不直接执行(它执行不了),而是回你一句:「请帮我调 getWeather('北京')」。
  • tool use 实操:注册一个工具,让模型来点菜:易踩坑:call.function.arguments 是字符串,不是 dict,要 json.loads 才能用——这点和第 24 篇「模型返回 JSON 字符串」一脉相承。
  • 把结果喂回去:一轮完整的工具调用闭环:拿到模型的「点菜单」后,流程是:① 真正执行函数;
  • ReAct:把单轮闭环变成「带记忆的循环」:ReAct(Reason + Act,推理 + 行动)就是这件事的范式:让模型在一个循环里反复「思考下一步 → 行动(调工具)→ 观察结果 → 再思考」,直到它认为任务完成、不再点菜为止。
  • 不用裸写循环:框架与现成方案:真实项目里你通常会用现成框架,它们把循环、工具注册、错误处理、历史管理都封好了——就像前端你不会裸写 XMLHttpRequest,而是用 axios / React Query。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“Agent 与工具调用”中,需要同时满足“零、本篇在阶段五里的位置”与“先建立前端锚点:tool use ≈ 给模型一张「函数注册表」”。给定正文约束“这是阶段五(AI 编程)依赖链的终点,前四篇全是它的地基。”,哪些判断保持了原有处理机制?多选
2“Agent 与工具调用”出现偏差:“在“Agent 与工具调用 / tool use 实操:注册一个工具,让模型来点菜”中,即使不满足“call.function.arguments 是字符串,不是 dict,要 json.loads 才能用——这点和第 24 篇「模型返回 JSON 字符串」一脉相承”,结果与副作”已成为实际行为。围绕“tool use 实操:注册一个工具,让模型来点菜”与“把结果喂回去:一轮完整的工具调用闭环”,哪些判断能定位被改变的职责或边界?多选
3评审“Agent 与工具调用”方案时,验收条件包含“① 每一圈都是一次付费 API 请求,循环 5 圈就是 5 次调用,token 还在累积变贵,所以 maxsteps 不是可选项而是必需的安全阀(前端死循环顶多卡浏览器,这里死循环是烧钱);”。关于“ReAct:把单轮闭环变成「带记忆的循环」”与“多步推理:循环跑起来后到底发生了什么”的哪些决策符合正文机制?多选