Day 12 / 共 20 天 · 第 3 周 Agent 大脑

工具体系

工具(tool)是 Agent 的"双手"。今天讲一个工具由什么组成、LLM 怎么选中并调用它、几个核心工具的门道,以及 MCP 如何让 Agent 接入海量外部工具。昨天(Day 11)我们看清了 Agent 的 step 主循环——每一步都会"选一个工具来用";今天就把那个被选用的"工具"本身拆开看,为明天(Day 13)讲"工具被选中后到底在哪、由谁真正执行"埋好伏笔。

📍 第 3 周 Agent 大脑(SDK)· 你在这里
Day11 SDK概念Day12 工具体系Day13 RuntimeDay14 LLM&提示词Day15 CodeAct
L01

工具 = 三件套

代码位置:工具实现在外部包 openhands-tools==1.34.0。本课结合前端里的 Action/Observation 类型(工具的"输入输出契约")+ 通用工具设计讲清原理。
💡 本质:工具就像瑞士军刀上的每一件小工具。Agent 腰上别着一把瑞士军刀,上面有刀、剪、锉、开瓶器……每一件都刻着"我叫什么、我干嘛用、怎么用"(名字+描述+参数说明书),还连着"抽出来真能用"的那截金属(执行函数)。本篇后面所有工具(bash/文件/浏览器/MCP)都在这个"瑞士军刀 + 说明书"的世界观里讲——每件工具 = 一个刀片 + 一张说明卡 + 一段真能干活的钢。

任何一个工具,本质是三样东西:

组成是什么例(bash 工具)
名字 + 描述给 LLM 看的"这工具干嘛用"execute_bash:"执行 bash 命令"
参数 schema调用时要填哪些参数(JSON Schema)command, is_input, timeout
执行函数真正干活的代码,产出 Observation在沙箱跑命令、返回输出
工具和 Action 什么关系? 一一对应!工具的"参数 schema"= 对应 Action 的字段(Day 02)。LLM 调用 execute_bash 工具、填 command="ls" → SDK 生成一个 ExecuteBashAction{command:"ls"} → 执行函数在沙箱跑它 → 产出 ExecuteBashObservation工具是"能力的定义",Action 是"一次具体调用",Observation 是"调用结果"。三位一体。
L02

参数 schema 是关键

💡 简化版 → 真实版对照如果让你"描述一个工具",你大概会写 {"name":"跑命令"} 就完事。真实的工具定义要长得多——多出来的每一段,都是为了让 LLM 少犯错。
// 如果你只写这样(朴素版):
{"name": "bash"}
// 问题:LLM 不知道它干嘛、要填什么参数、哪个必填 → 乱调、填错

真实版(OpenAI function calling 格式,发给 LLM,即 Day 03 的 SystemPromptEvent.tools)把这些补齐:

{
  "type": "function",
  "function": {
    "name": "execute_bash",
    "description": "在沙箱里执行一条 bash 命令",
    "parameters": {
      "type": "object",
      "properties": {
        "command": {"type": "string", "description": "要执行的命令"},
        "is_input": {"type": "boolean", "description": "是否向运行中的进程输入"}
      },
      "required": ["command"]
    }
  }
}
读法:这段 JSON 就是"教 LLM 怎么用这个工具"的说明书——工具叫什么、干嘛、有哪些参数、哪些必填。LLM 读了它,就知道"想跑命令时,我该输出一个 name=execute_bash、command 填某值的 tool call"。
为什么 description 写得好坏很重要? LLM 完全靠 description 理解"什么时候该用这个工具、参数该填什么"。描述含糊,LLM 就会乱用或填错参数。所以工具的 name/description/参数说明本质是一种"给 AI 的提示词工程"——写清楚了,Agent 才聪明。这也是为什么好的 Agent 框架在工具描述上很下功夫。
⚠️ 小白常误以为:工具的 description 就是"给人看的注释",随便写写、甚至不写也行。其实:它是喂给 LLM 的正式输入,模型完全靠它判断该不该用、参数填什么。写错一个词,Agent 就可能在该用 A 工具时用了 B。它更像军刀上那张"官方说明卡",不是可有可无的备注。
L03

LLM 如何"选中"工具

🤔 对话体:LLM 是"猜"该用哪个工具吗? 👶 小白:模型怎么知道该抽瑞士军刀上的哪一片?靠猜?
👨‍🏫 老师:不靠猜。每次请求,SDK 会把全部工具的"说明卡"(JSON 定义)连同对话历史一起递给它,相当于把整把军刀摊开摆它面前。
👶 小白:那它怎么"抽"出来?
👨‍🏫 老师:它不是真的抽——它在回复里结构化地写一段 tool_calls:「我要 name=execute_bash,command='ls'」。SDK 按这个 name 找到对应工具,把 arguments 解析成 Action。
👶 小白:万一它写了个不存在的工具名?
👨‍🏫 老师:那就配不上任何工具、执行不了——所以说明卡(description/参数 schema)写得越清楚,模型越不会"抽错片"。

流程(Day 11 的 step 细化):① 把所有工具的 JSON 定义 + 当前对话历史发给 LLM;② LLM 回复里带一个(或多个)tool_calls,指明"调哪个工具、参数是什么";③ SDK 按 tool_call.function.name 找到对应工具,把 arguments 解析成 Action。

可能一次调多个工具:Day 03 的 ActionEvent 有个 llm_response_id 字段——同一次 LLM 响应里的多个并行动作用它归组。比如 LLM 一次说"同时读这三个文件",就产生三个并行的 ActionEvent。并行工具调用能加速任务(一次 LLM 往返干多件事)。每个工具调用还带 tool_call_id,结果 Observation 用 action_id 配对回去(Day 03)。
📝 举个例子LLM 回复里带 tool_calls:[{name:"execute_bash", arguments:{command:"ls"}}] → SDK 生成 ExecuteBashAction{command:"ls"} → 在沙箱执行 → 产出 ExecuteBashObservation{exit_code:0, output:"README.md src/"} → 用 action_id 配对追加回历史。一次"抽刀→用刀→看结果"就完成了。
LLM tool_callname+arguments Action按 name 解析 执行函数在沙箱干活 Obs 工具三件套:说明卡(名字+schema)驱动前半段,执行函数兑现后半段
图:一次工具调用的生命线——tool_call → Action → 执行 → Observation
L04

bash 工具(最核心)

对应 ExecuteBashAction(Day 02)。它不是"跑一次性命令"那么简单——它维护一个持久的终端会话

  • 命令在同一个 shell 会话里执行,所以 cd 之后的路径、设的环境变量都保留(有状态)。
  • is_input=true:向正在运行的交互程序输入(如回答 y/n)。
  • reset=true:终端卡死时重建会话。
  • 结果 ExecuteBashObservationexit_code、输出内容、还从终端提示符 PS1 抓取当前工作目录等元数据。
为什么要"持久终端"而不是每次开新的? 因为真实工作是有上下文的:你 cd project && source venv/bin/activate 之后,希望后续命令都在这个目录、这个虚拟环境里跑。如果每条命令都开一个全新 shell,这些状态就丢了。持久终端让 Agent 像人一样"在一个终端里连续操作"。从 PS1 抓工作目录这种细节,是为了让 Agent 始终知道"我现在在哪个目录"。
L05

文件编辑工具(str_replace 模式)

对应 FileEditorAction(Day 02 精读过)。5 个子命令:view(看)、create(建)、str_replace(查找替换)、insert(插入)、undo_edit(撤销)。结果 FileEditorObservationold_content/new_content(改前改后对比)。

为什么 Observation 要带"改前改后"? ① 前端能渲染成 diff(绿加红删),你一眼看清 Agent 改了啥;② Agent 自己也能核对"我这次改动符合预期吗";③ undo_edit 能靠它回滚。Day 02 讲过 str_replace 省 token、防误伤——今天补上:它的 Observation 设计还服务于"可视化 + 可核对 + 可撤销"。好工具不仅动作设计好,结果反馈也设计好。
L06

浏览器工具

对应 Browser*Action(10 个,如 navigate/click/type)。底层用 browsergym + playwright(真实浏览器自动化,见 pyproject.toml)。结果 BrowserObservationoutputscreenshot_data(base64 网页截图)。

Agent 怎么"看"网页? 两条信息:① 网页的可访问性树/文本(哪些按钮、链接、输入框,带编号)——LLM 靠它决定"点第几个元素";② 截图——多模态 LLM 能"看图"理解页面布局。所以浏览器 Observation 既给文本又给截图。让 Agent 上网查文档、复现网页 bug、甚至操作 Web 应用——这大大扩展了它能干的活。需要 enable_browser 打开(Day 04)。
L07

MCP:接入海量外部工具

对应 MCPToolAction(Day 02)。MCP(Model Context Protocol)是一个开放协议,让工具/数据源用标准方式暴露给 AI。OpenHands 支持接入 MCP server(配置 [mcp] 段,支持 sse/shttp/stdio 三种传输)。

MCP 的意义:工具生态标准化。有了 MCP 适配,社区里海量的 MCP server(数据库、文件系统、GitHub、Slack、各种 SaaS……就是本教程运行环境里那些 mcp__* 工具用的协议)都能直接变成你 OpenHands Agent 的工具,你几乎不用自己写。fastmcp+mcp 是依赖(pyproject.toml)。"定义标准接口 → 生态自动繁荣"——和 eino 的 eino-ext、组件接口是同一个哲学。接口标准化是生态的地基。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 一个工具由哪三件套组成?和 Action/Observation 什么关系?
  • 参数 schema 为什么关键?description 写好坏为什么影响大?
  • LLM 怎么选中并调用工具?并行工具调用靠什么归组?
  • bash 工具为什么要"持久终端"?文件工具的 Observation 为什么带改前改后?
  • MCP 带来什么?和 eino-ext 哪里像?

✋ 动手

# 对照 Action 类型看工具契约
sed -n '25,214p' frontend/src/types/v1/core/base/action.ts | head -60
# MCP / 浏览器依赖
grep -iE 'mcp|browsergym|playwright' pyproject.toml
# 配置里的 MCP 段
sed -n '/\[mcp\]/,/^\[/p' config.template.toml | head -30
明天预告 · Day 13Runtime 运行时——Action 产生后,怎么被送进沙箱真正执行、Observation 怎么回来。运行时抽象、本地/Docker/远程三种实现,以及它和沙箱的关系。
← Day 11 SDK Day 13 · Runtime 运行时 →