
字节笔记本
2026年10月24日 · 约 18 分钟读完
把 Agent 当工具用:嵌套调用

本文是《从 0 写 Agent》系列第 18 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「Agent as Tool:嵌套调用」。
Handoff 是"切权",目标 Agent 接管整个 Run。但有时候你不想交接控制权,而是想让主 Agent"问一下"子 Agent,拿到结果继续自己干。
这就是 Agent-as-Tool,把一个 Agent 当工具用。主 Agent 调用它,子 Agent 跑一轮,返回结果,控制权归还。这一章拆它和 Handoff 的本质区别,以及嵌套 Run 的继承策略。
引子:什么时候用 Agent-as-Tool
想象一个"研究助手"主 Agent,它能查资料、写报告。其中"查资料"是个复杂能力,要搜索、筛选、总结。你不想把这个能力塞进主 Agent(太臃肿),而是做成一个专门的"研究子 Agent"。
期望的交互:
用户 → 主 Agent:"写一份关于 Agent 框架的报告"
主 Agent → 调用 researchAgent(当工具):"查 Agent 框架的最新进展"
researchAgent → 跑一轮(搜索、筛选、总结),返回结果
主 Agent → 拿到研究结果,继续写报告主 Agent 没有交出控制权,它只是"调用"了 researchAgent,拿到结果继续。这和 Handoff 完全不同。

Handoff vs Agent-as-Tool:拓扑对比
| 维度 | Handoff(切权) | Agent-as-Tool(嵌套) |
|---|---|---|
| 控制权 | 转交,原 Agent 退出 | 不转交,子 Agent 跑完归还 |
| 子 Agent 看到的历史 | 完整(或过滤后的)对话历史 | 只有传入的 input 参数 |
| 子 Agent 的输出 | 直接返回给用户 | 返回给主 Agent 当工具结果 |
| 主 Agent 是否继续 | 不继续(已退出) | 继续,处理子 Agent 的结果 |
| 拓扑 | 线性接力(A→B→C) | 树状调用(A 调 B、C、D) |
| 类比 | 转接客服(原客服挂线) | 主客服请教专家再回复 |
核心区别,子 Agent 看到多少上下文。 Handoff 的目标 Agent 看完整历史(它要接管整个对话);Agent-as-Tool 的子 Agent 只看到传入的 input(它是个"黑盒工具",不需要知道全局)。
源码对照:AsTool 方法
AsTool(agent_tool.go:37)把一个 Agent 变成 Tool:
// agents/agent_tool.go:37(骨架)
func (a *Agent) AsTool(cfg AgentToolConfig) Tool {
return &FunctionTool{
Name: cfg.Name, // 默认用 agent 名
Description: cfg.Description,
ParamsJSONSchema: schemaOfAgentToolInput, // {"input": "string"}
OnInvoke: func(ctx, tc, argsJSON string) (any, error) {
var args agentToolInput // {Input string}
json.Unmarshal([]byte(argsJSON), &args)
// 关键:递归调用 Run,启动嵌套 Agent 循环
nestedOpts := nestedRunOptions(tc.RunContext, cfg.MaxTurns)
res, err := Run(ctx, a, args.Input, nestedOpts)
if err != nil {
return nil, err
}
// 返回子 Agent 的最终输出
return res.FinalOutputString(), nil
},
}
}关键三步:
- 工具的参数 schema 是
{input: string},调用方只传一个输入字符串 OnInvoke里递归调用Run,启动一个全新的嵌套 Agent 循环- 把子 Agent 的最终输出作为工具结果返回给主 Agent
注意,子 Agent 收到的只有 args.Input(传入的字符串),不是完整对话历史。 这就是"黑盒工具"的语义,子 Agent 不知道主对话发生了什么,只处理传给它的任务。
参数 schema 为什么这么简单
// agents/agent_tool.go:25
type agentToolInput struct {
Input string `json:"input" jsonschema:"The input to pass to the agent"`
}只有一个 input 字符串。 为什么不传结构化参数?
因为 Agent-as-Tool 的语义是"委托一个自然语言任务",主 Agent 用自然语言描述要子 Agent 干什么,子 Agent 自己理解。这比"传一堆结构化字段"灵活,主 Agent 说"查一下最近 Agent 框架的进展并总结要点",子 Agent 自己决定怎么查、怎么总结。
如果子 Agent 需要结构化参数,那它就不该用 Agent-as-Tool,而该用普通工具。 Agent-as-Tool 的价值就是"处理模糊的自然语言任务"。
源码对照:嵌套 Run 的继承策略
nestedRunOptions(agent_tool.go:73)决定子 Agent 从父 Run 继承什么:
// agents/agent_tool.go:73(骨架)
func nestedRunOptions(parent *RunContext, maxTurns int) RunOptions {
opts := RunOptions{MaxTurns: maxTurns}
if parent != nil && parent.inheritedOpts != nil {
// 继承这些
opts.ModelProvider = parent.inheritedOpts.ModelProvider
opts.Model = parent.inheritedOpts.Model
opts.ModelSettings = parent.inheritedOpts.ModelSettings
opts.Tracer = parent.inheritedOpts.Tracer
}
if parent != nil {
opts.parentTrace = parent.activeTrace // trace 归到同一 workflow
opts.Context = parent.Context // 共享用户 context
}
return opts
}继承的(4 + 2 项)
| 继承项 | 为什么 |
|---|---|
ModelProvider | 子 Agent 用同一个 provider(不用自备 API key) |
Model | 同一个模型配置 |
ModelSettings | 同样的温度等设置 |
Tracer | trace 归到父 Run 的 workflow |
parentTrace | 子 Run 的 span 挂在父 trace 下(不孤立) |
Context | 共享用户的 context(自定义数据) |
继承让子 Agent "开箱即用",不用自己配模型、不用自己配 trace。 主 Agent 配一次,所有子 Agent 共享。
不继承的(2 项),关键的隔离
注释明说(agent_tool.go:71):"fresh approval store so nested approvals don't leak into the parent",新建审批存储,嵌套审批不泄漏到父级。
两个关键的不继承:
-
Usage(token 计量),子 Run 新建 usage 累加器。为什么?因为子 Agent 的 token 消耗应该独立计量,方便追踪"这个子任务花了多少"。
-
ApprovalStore(审批决策),子 Run 新建审批存储。这极其重要,如果继承父级的审批,子 Agent 调用危险工具时会用父级的"always approved"决策,绕过审批。隔离审批存储保证子 Agent 的危险操作仍然要单独审批。
这是安全设计,嵌套不能成为绕过审批的后门。
设计权衡:为什么 trace 要合并
注意 opts.parentTrace = parent.activeTrace(agent_tool.go:84),子 Run 的 trace 挂在父 trace 下,不另起根 trace。
为什么不独立? 因为从用户视角,"主 Agent 调用子 Agent"是一个完整的工作流。如果子 Run 另起根 trace,trace 视图里会看到一个孤立的子 Run,看不出它和主 Run 的关系。
合并后,trace 视图是树状的:
trace: main workflow
├── span: main agent
│ ├── span: tool_call (research_agent)
│ │ ├── span: nested agent ← 子 Run 的 span
│ │ │ ├── span: tool_call (search)
│ │ │ └── span: tool_call (summarize)
│ └── span: tool_call (write_report)一目了然,主 Agent 调了 research_agent,research_agent 内部又调了 search 和 summarize。 这是分布式追踪的标准做法(parent span + child span)。
第 25-27 章讲 Tracing 时会详述这个父子 span 机制。
设计权衡:嵌套深度与 MaxTurns
嵌套 Run 可以无限递归,主 Agent 调子 Agent,子 Agent 又调孙 Agent。这有风险,无限嵌套会栈溢出、token 爆炸。
AgentToolConfig.MaxTurns(agent_tool.go:17)限制子 Agent 的轮次:
type AgentToolConfig struct {
MaxTurns int // 0 = 用 DefaultMaxTurns
}建议给子 Agent 设较小的 MaxTurns(比如 3-5),因为子任务通常比主任务简单:
researchTool := researchAgent.AsTool(agents.AgentToolConfig{
Name: "research",
Description: "查资料并总结",
MaxTurns: 5, // 子任务最多 5 轮
})如果子 Agent 也调用 Agent-as-Tool,深度会叠加,主 10 轮 × 子 5 轮 × 孙 5 轮 = 最坏 250 轮。要控制嵌套深度,通常限制到 2-3 层。
CustomOutputExtractor:自定义返回内容
默认情况下,子 Agent 的最终输出(res.FinalOutputString())作为工具结果返回。但你可以用 CustomOutputExtractor(agent_tool.go:20)定制:
researchTool := researchAgent.AsTool(agents.AgentToolConfig{
CustomOutputExtractor: func(res *agents.RunResult) (string, error) {
// 不返回完整输出,只返回关键结论
return extractKeyFindings(res), nil
},
})什么时候用? 当子 Agent 的完整输出太长或格式不适合直接给主 Agent 时。比如研究 Agent 产出一份 2000 字报告,但主 Agent 只需要"3 个关键发现",用 extractor 提取精简版。
什么时候用 Handoff、什么时候用 Agent-as-Tool
这是 Part III 最实用的决策。三个判断维度:
维度一:控制权要不要回来
- 要回来(主 Agent 继续主导)→ Agent-as-Tool
- 不要(目标 Agent 接管到底)→ Handoff
维度二:子任务是不是"黑盒"
- 黑盒(主 Agent 不关心子 Agent 怎么干,只要结果)→ Agent-as-Tool
- 非黑盒(目标 Agent 要看完整对话上下文)→ Handoff
维度三:是"一次性委托"还是"接力"
- 一次性委托(查个资料、生成个摘要)→ Agent-as-Tool
- 接力(客服转专员、专员转主管)→ Handoff
实战示例
| 场景 | 选择 | 理由 |
|---|---|---|
| 客服转退款专员 | Handoff | 接力,退款专员要接管对话 |
| 主 Agent 调研究 Agent 查资料 | Agent-as-Tool | 委托,主 Agent 要结果继续干 |
| 分诊转急诊医生 | Handoff | 接力 |
| 写作 Agent 调校对 Agent 检查 | Agent-as-Tool | 委托,拿到校对结果继续改 |
| 第一线支持转二线支持 | Handoff | 接力 |
经验法则,"转接"用 Handoff,"咨询"用 Agent-as-Tool。
小结
读完这一章,你应该理解:
- Agent-as-Tool 的本质,把 Agent 包装成工具,主 Agent 调用它,子 Agent 跑一轮返回结果,控制权归还
- 和 Handoff 的核心区别,子 Agent 只看传入 input(黑盒),不继承对话历史
- 继承策略,继承 ModelProvider/Tracer(开箱即用),不继承 Usage/ApprovalStore(安全隔离)
- trace 合并,子 Run 的 span 挂在父 trace 下,保持 workflow 完整视图
- 选择标准,"转接"用 Handoff,"咨询"用 Agent-as-Tool
Agent-as-Tool 让多 Agent 协作有了第二种拓扑,树状委托,而非线性接力。 两种模式组合,能覆盖绝大多数多 Agent 场景。
下一篇
第 19 章,Lifecycle Hooks:运行时的事件钩子。Agent 循环的各个关键节点(开始、调模型、调工具、交接、结束)都能挂钩子。agents-go 有 Run 级和 Agent 级两套 hook,共 7 个回调点。这是做监控、日志、指标收集的基础。
本文是《从 0 写 Agent》系列第 18 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。
