ByteNoteByteNote
把 Agent 当工具用:嵌套调用
字

字节笔记本

2026年10月24日 · 约 18 分钟读完

把 Agent 当工具用:嵌套调用

API中转
¥120

Subagent:并行与任务拆分

本文是《从 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"。

期望的交互:

text
用户 → 主 Agent:"写一份关于 Agent 框架的报告"
主 Agent → 调用 researchAgent(当工具):"查 Agent 框架的最新进展"
researchAgent → 跑一轮(搜索、筛选、总结),返回结果
主 Agent → 拿到研究结果,继续写报告

主 Agent 没有交出控制权,它只是"调用"了 researchAgent,拿到结果继续。这和 Handoff 完全不同。


Agent 循环:一切的基础

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:

go
// 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
        },
    }
}

关键三步:

  1. 工具的参数 schema 是 {input: string},调用方只传一个输入字符串
  2. OnInvoke 里递归调用 Run,启动一个全新的嵌套 Agent 循环
  3. 把子 Agent 的最终输出作为工具结果返回给主 Agent

注意,子 Agent 收到的只有 args.Input(传入的字符串),不是完整对话历史。 这就是"黑盒工具"的语义,子 Agent 不知道主对话发生了什么,只处理传给它的任务。

参数 schema 为什么这么简单

go
// 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 继承什么:

go
// 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同样的温度等设置
Tracertrace 归到父 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",新建审批存储,嵌套审批不泄漏到父级。

两个关键的不继承:

  1. Usage(token 计量),子 Run 新建 usage 累加器。为什么?因为子 Agent 的 token 消耗应该独立计量,方便追踪"这个子任务花了多少"。

  2. 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 视图是树状的:

text
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 的轮次:

go
type AgentToolConfig struct {
    MaxTurns int  // 0 = 用 DefaultMaxTurns
}

建议给子 Agent 设较小的 MaxTurns(比如 3-5),因为子任务通常比主任务简单:

go
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)定制:

go
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。


小结

读完这一章,你应该理解:

  1. Agent-as-Tool 的本质,把 Agent 包装成工具,主 Agent 调用它,子 Agent 跑一轮返回结果,控制权归还
  2. 和 Handoff 的核心区别,子 Agent 只看传入 input(黑盒),不继承对话历史
  3. 继承策略,继承 ModelProvider/Tracer(开箱即用),不继承 Usage/ApprovalStore(安全隔离)
  4. trace 合并,子 Run 的 span 挂在父 trace 下,保持 workflow 完整视图
  5. 选择标准,"转接"用 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 移植版)。全系列目录见总览。

相关文章

分享: