Tool 工具使用文档
Tool 工具系统是 tRPC-Agent-Go 框架的核心组件,为 Agent 提供了与外部服务和功能交互的能力。框架支持多种工具类型,包括函数工具和基于 MCP(Model Context Protocol)标准的外部工具集成。
概述
🎯 核心特性
- 🔧 多类型工具:支持函数工具(Function Tools)和 MCP 标准工具
- 🌊 流式响应:支持实时流式响应和普通响应两种模式
- ⚡ 并行执行:工具调用支持并行执行以提升性能
- 🔄 MCP 协议:完整支持 STDIO、SSE、Streamable HTTP 三种传输方式
- 🔁 单次调用重试:支持在 LLMAgent / Graph ToolsNode 上为单次
CallableTool调用配置重试 - 🛠️ 配置支持:提供配置选项和过滤器支持
- 🧹 参数修复:可选启用
agent.WithToolCallArgumentsJSONRepairEnabled(true),对tool_calls的arguments做一次尽力 JSON 修复,提升工具执行与外部解析的鲁棒性
核心概念
🔧 Tool(工具)
Tool 是单个功能的抽象,实现 tool.Tool 接口。每个 Tool 提供特定的能力,如数学计算、搜索、时间查询等。
建议(务必配置 name 与 description)
- name(必填):用于让模型精确定位要调用的工具。请保证 稳定、唯一、语义明确(建议使用
snake_case),不要在不同工具/不同 ToolSet 之间重名。 - description(必填):用于让模型理解“这个工具做什么/何时该用/有什么约束”。没有清晰的描述会显著降低 tool call 的命中率与稳定性。
对于 Function Tool:通过
function.WithName(...)/function.WithDescription(...)配置;对于自定义 Tool:在Declaration()返回的tool.Declaration中设置Name/Description。
🛡️ Tool Metadata 与权限策略
Tool Metadata 是可选的执行行为描述,不改变核心 tool.Tool 接口。
当宿主、策略或 UI 需要判断一个工具是否只读、是否具有破坏性、是否可并发、是否主要用于搜索/读取、是否会访问外部世界,或是否声明了建议的结果大小上限时,可以让工具实现 metadata。
直接使用 MCP ToolSet 暴露出来的工具,也会从显式 MCP annotations 生成 metadata:
| MCP annotation | ToolMetadata 字段 |
|---|---|
readOnlyHint |
ReadOnly |
destructiveHint |
Destructive |
openWorldHint |
OpenWorld |
框架只映射 MCP server 显式返回的 hint。如果 MCP server 没有返回
destructiveHint 或 openWorldHint,ToolMetadata 会保持 Go 零值
(false)。这与 MCP 规范的默认 hint 语义不同:MCP 中非只读工具的
destructiveHint 默认是 true,openWorldHint 默认也是 true。
由于 ToolMetadata 使用普通 bool 字段,无法区分“未设置”和“显式
false”。如果你的 permission policy 需要遵循 MCP 默认语义,请对非只读
MCP 工具采用更保守的策略,除非业务侧还有额外的可信信号。
MCP annotations 没有与 SearchOrRead 或 ConcurrencySafe 对应的字段。
框架也不会把 readOnlyHint 或 idempotentHint 推断为并发安全信号。
权限策略发生在模型已经发起 tool call、框架完成 JSON 修复和 before-tool callbacks 参数改写、并且即将真正执行工具之前:
工具也可以实现 tool.PermissionChecker 来声明自己的强约束。工具级检查先于本次运行的 policy 执行,遇到第一个非 allow 决策就停止。没有实现 checker 的工具,在配置了本次运行 policy 时仍然会被 policy 检查;如果工具 checker 和本次运行 policy 都不存在,则保持旧行为,默认允许执行。
决策语义:
tool.AllowPermission():执行工具。tool.DenyPermission(reason):不执行工具,并向模型返回结构化的denied工具结果。tool.AskPermission(reason):不执行工具,并向模型返回结构化的approval_required工具结果。
如果你的应用有真正的审批 UI,请在 policy 内部完成询问,并在用户同意后返回 tool.AllowPermission()。框架不会为 ask 自行发明一套 UI 交互流程。
几个机制的边界需要区分清楚:
agent.WithToolFilter(...):控制哪些工具对模型可见。agent.WithToolExecutionFilter(...):让部分已可见的 tool call 留给调用方外部执行。agent.WithToolPermissionPolicy(...):对框架即将执行的每个工具做权限判断。- Tool callbacks 与 guardrail plugins 仍然适合做鉴权、审计、自动审批评估等流程。简单确定性的 allow/deny/ask 判断,优先使用 permission policy。
📦 ToolSet(工具集)
ToolSet 是一组相关工具的集合,实现 tool.ToolSet 接口。ToolSet 负责管理工具的生命周期、连接和资源清理。
Tool 与 ToolSet 的关系:
- 一个 "Tool" = 一个具体功能(如计算器)
- 一个 "ToolSet" = 一组相关的 Tool(如 MCP 服务器提供的所有工具)
- Agent 可以同时使用多个 Tool 和多个 ToolSet
🌊 流式工具支持
框架支持流式工具,提供实时响应能力:
流式工具特点:
- 🚀 实时响应:数据逐步返回,无需等待完整结果
- 📊 大数据处理:适用于日志查询、数据分析等场景
- ⚡ 用户体验:提供即时反馈和进度显示
工具类型说明
| 工具类型 | 定义 | 集成方式 |
|---|---|---|
| Function Tools | 直接调用 Go 函数实现的工具 | Tool 接口,进程内调用 |
| Agent Tool (AgentTool) | 将任意 Agent 包装为可调用工具 | Tool 接口,支持流式内部转发 |
| DuckDuckGo Tool | 基于 DuckDuckGo API 的搜索工具 | Tool 接口,HTTP API |
| MCP ToolSet | 基于 MCP 协议的外部工具集 | ToolSet 接口,支持多种传输方式 |
📖 相关文档:Agent 间协作相关的 Agent Tool 和 Transfer Tool 请参考 多 Agent 系统文档。
Function Tools 函数工具
Function Tools 通过 Go 函数直接实现工具逻辑,是最简单直接的工具类型。
基本用法
Input Schema(入参 schema)与字段描述
Function Tool 的入参 req 会自动生成对应的 JSON Schema(用于模型理解参数结构)。建议通过 struct tag 补充字段描述:
- 字段名:使用
json:"..."作为 schema 的字段名。 - 字段描述(推荐):使用
jsonschema:"description=..."写入 schema 的properties.<field>.description。 - 注意:
jsonschematag 内部使用英文逗号,作为分隔符,因此 description 内容中不能包含,,否则会被误解析成多个 tag。 - 兼容:也支持
description:"..."作为字段描述(用于历史代码);若同时配置jsonschema:"description=..."与description:"...",以jsonschema中的description为准。 - 更灵活的 schema:如果想完全自定义入参 schema(例如需要更复杂的 JSON Schema 结构/约束),可使用
function.WithInputSchema(customInputSchema)跳过自动生成。
流式工具示例
在 Tool 实现里获取 Tool Call ID
当模型发出一条 tool_call 后,框架会在真正执行工具前,把这次调用的
tool_call_id 注入到工具执行的 context.Context 中。
这意味着:在你自己的 Tool 实现里,框架支持直接读取这条 ID。
这个能力对以下场景特别有用:
- 同名工具并发调用时,为每次调用生成不冲突的状态键
- 给日志、监控、埋点、trace 打上稳定的工具调用标识
- 当工具内部再触发一个子 Agent 时,把“这个子 Agent 来自哪条 tool_call”传给 UI 或上层编排逻辑
当前这套机制适用于:
- LLMAgent 中的普通函数工具
- LLMAgent 中的流式工具
- GraphAgent 的工具执行节点
- Tool callbacks / plugins(回调参数里也会带
ToolCallID)
最直接的用法
在工具实现中调用 tool.ToolCallIDFromContext(ctx) 即可:
如果你只是想在 Tool 里打印日志、做指标、写 Invocation State,通常到这里就够了。
完整可运行示例可参考 examples/toolcallid。
当 Tool 内部还要拉起子 Agent 时
这里要先区分两个概念:
tool_call_id:模型发出的“这一条工具调用”的 IDInvocationID/ParentInvocationID:Agent 执行树里的父子调用关系
如果你的目标是“让 UI 把子 Agent 的输出挂到主 Agent 的某条工具调用下面”, 推荐把这两层信息都保留下来:
- 用
tool.ToolCallIDFromContext(ctx)取到当前tool_call_id - 用
agent.InvocationFromContext(ctx)取到父 Invocation - 用
parentInv.Clone(...)创建子 Invocation - 把
tool_call_id放进子 Invocation 的RunOptions.RuntimeState - UI 侧同时使用:
InvocationID/ParentInvocationID建立调用树- 你传下去的
tool_call_id绑定“来源于哪条 tool_call”
示例(假设 childAgent 已经是一个可运行的子 Agent 实例):
写入前先复制一份 RuntimeState。Invocation.Clone(...)
不会对 map 做深拷贝;如果直接复用并写入,就会连父
Invocation 一起改掉。
子 Agent 内部如果还需要继续读取这个“来源 tool_call_id”,可以直接从 运行时状态里拿:
推荐实践
- 如果你只需要“这一条工具调用”的标识,直接用
tool.ToolCallIDFromContext(ctx) - 如果你要表达“子 Agent 是谁触发的”,不要只依赖
tool_call_id一个字段;调用树请优先看InvocationID/ParentInvocationID - 如果 UI 还要回挂到“具体哪条工具卡片”,再把
tool_call_id通过RuntimeState或自定义事件元数据显式传下去 - 如果你用的是
AgentTool,框架已经会用Invocation.Clone(...)维护父子 Invocation 关系;UI 侧通常已经能看到清晰的父子调用树。 只有在你还想把子树额外绑定回某条 tool card 时,才需要再传tool_call_id
一个容易忽略的细节
框架会在执行工具前把 tool_call_id 注入到 context。
但如果你的 BeforeTool 回调主动返回了一个全新的裸 Context
(没有保留原值),那后续工具代码里就拿不到这个 ID 了。
因此,如果你会在回调里替换 context,记得把已有的 context value 一并透传。
内置工具类型
Tool 调用重试
当工具调用可能因为瞬时问题失败时,可以为它配置重试,例如:
- 下游网络波动;
- 临时超时;
- 外部服务偶发异常。
这项能力默认关闭。当前仅对 CallableTool 生效,StreamableTool 暂不支持。开启后,框架只会重试当前这次工具调用,不会重跑整个 Agent 或整轮 Graph 工作流。
基本配置
常用字段:
MaxAttempts:总尝试次数,包含第一次调用。InitialInterval:第二次尝试前的初始等待时间。BackoffFactor:失败后退避倍数。MaxInterval:等待时间上限。Jitter:是否开启抖动。
默认判定规则
如果未提供 RetryOn,框架会使用 tool.DefaultRetryOn(...)。
默认规则比较保守,只会重试常见的瞬时错误,例如:
io.EOFio.ErrUnexpectedEOFnet.Error中的 timeout / temporary 错误
像 context.Canceled、context.DeadlineExceeded 以及结果级失败,默认不会重试。
自定义重试条件
如果默认规则不够,可以通过 RetryOn 自定义判定逻辑。推荐先复用 tool.DefaultRetryOn(...),再补充自己的条件:
tool.RetryInfo 里会带上当前调用的信息,例如工具名、当前是第几次尝试、原始错误、结果级失败标记等,方便你在 RetryOn 中做判断。
在 LLMAgent 中启用
可运行示例:
在 Graph 中启用
可运行示例:
DuckDuckGo 搜索工具
DuckDuckGo 工具基于 DuckDuckGo Instant Answer API,提供事实性、百科类信息搜索功能。
基础用法
高级配置
Claude Code ToolSet
tool/claudecode 提供了一组面向代码工作的 ToolSet,用于在框架内部暴露与 Claude Code 接近的工具接口。它覆盖文件读写、代码检索、命令执行和网页获取等能力,可以直接挂接到 LLMAgent 或其他运行时。如果你的目标是调用本地 Claude Code CLI,并消费 CLI 的执行轨迹与工具事件,请参考 Claude Code Agent 使用指南。
从能力组成上看,claudecode 默认会提供一组代码工作流工具,包括 Bash、TaskStop、TaskOutput、Read、Glob、Grep、WebFetch 和 WebSearch。在非只读模式下,还会额外提供 Write、Edit 和 NotebookEdit。
下表列出了当前 claudecode 工具集中的主要工具及其用途:
| 工具名 | 说明 |
|---|---|
Bash |
执行本地 Shell 命令。 |
TaskStop |
停止由 Bash 以后台模式启动的任务。 |
TaskOutput |
读取后台任务的当前输出或最终输出。 |
Read |
读取文件内容。 |
Glob |
按路径模式查找文件。 |
Grep |
按内容搜索仓库。 |
WebFetch |
抓取指定 URL 的页面内容。 |
WebSearch |
进行开放式网页搜索。 |
Write |
创建文件或用完整内容覆盖文件,仅在非只读模式下暴露。 |
Edit |
对已有文本文件做局部替换,仅在非只读模式下暴露。 |
NotebookEdit |
按 cell 粒度编辑 .ipynb 文件,仅在非只读模式下暴露。 |
基本用法
llmagent.WithToolSets(...) 会以 ToolSet 形式接入这组工具;如果调用 Tools(),则会得到展开后的单个工具列表。
常用配置
tool/claudecode 的配置重点围绕工作目录、只读模式和 Web 能力展开:
| Option | 说明 |
|---|---|
WithName(name) |
覆盖 ToolSet 名称,默认值为 claudecode。 |
WithBaseDir(dir) |
指定工具集的基础目录。文件、检索和命令执行都会以此为基准。 |
WithReadOnly(readOnly) |
启用只读模式后,不再暴露 Write、Edit、NotebookEdit。 |
WithMaxFileSize(size) |
限制单个文件可读取的最大尺寸。 |
WithWebFetchOptions(opts) |
配置 WebFetch 的域名策略、超时与内容处理方式。 |
WithWebSearchOptions(opts) |
配置 WebSearch 的后端、分页参数与请求选项。 |
WithBaseDir 定义了 Read、Write、Edit、Glob、Grep 等文件相关工具的工作范围,也决定了 Bash 的默认执行目录。启用只读模式后,工具集只保留读取、检索、命令执行和 Web 相关能力;关闭只读模式后,会额外暴露 Write、Edit 与 NotebookEdit。
Todo 工具
Todo 工具为 Agent 提供一份结构化、可跨轮持久化的任务清单。模型通过 todo_write 发布或更新当前计划;清单会被持久化到 session、随 tool result 事件返回给前端,并在每次写入后默认追加一段简短提示,督促模型边推进边更新状态。
适合下面这些场景:
- 任务跨多个非平凡步骤,模型容易漏掉某一步;
- 用户或下游 UI 需要在 Agent 工作过程中看到可见的进度;
- 同一会话可能中途暂停(例如向用户追问信息),后续再续上原计划继续推进。
基础用法
todo.DefaultToolPrompt 是开箱即用的 system instruction 片段,告诉模型何时调用 todo_write 以及如何撰写条目;你也可以替换成自己的版本,下文的运行时校验规则不受影响。
强制完成
默认情况下,todo_write 只是建议性工具:即使清单里还有未完成项,模型仍可能直接结束回复。若希望 Agent 必须完成清单后才能输出最终回复,可以安装 todoenforcer extension:
该 extension 会自动贡献 todo_write 和 todo_declare_blocker,不要再通过 WithTools 额外传入 todo.New()。如果需要复用 tool/todo 的选项(例如 WithStateKeyPrefix、WithClearOnAllDone 或 WithNudgeHook),先构造 todo.New(...),再通过 todoenforcer.WithTodoTool(...) 传入。todo_declare_blocker 用于声明客观阻塞,例如缺少权限、凭据、基础设施或必须由用户决策的信息。
工具返回结构
todo_write 返回的是结构化结果而不是自由文本,因此终端、AG-UI、自研 HTTP 前端等任何调用方都可以直接消费:
Todos 和 OldTodos 会直接挂在 tool result 事件上,AG-UI 客户端(或任何消费 tool_call_result 的前端)解码后即可渲染最新状态和 diff,不需要回查 session。examples/todo/ 中的终端示例内联了一个小型 ASCII 格式化函数;富前端可以按自己的风格渲染同一份结构化数据。
跨轮持久化与 Branch 隔离
每次写入都会作为 session state delta 发布,因此清单可以跨 Runner.Run 调用持久化;当后端 session service 是持久化实现时,跨进程同样有效。
清单按 Invocation.Branch 分键存储;当调用方没有显式设置 branch 时,框架会自动把它补成该 Agent 的名称(因此子 Agent 或 agent-tool 会读写自己的清单,不会覆盖父 Agent 的计划)。对应上面的基础接入(llmagent.New("todo-assistant", ...)),服务端读取应当写成:
如果你配置了自定义 branch(例如通过 WithInvocationBranch,或把当前 Agent 作为子 Agent 运行在父 Agent 下),就传入对应的 branch 值。只有当 Invocation.Branch 被显式置为 "" 时,传空字符串才会命中顶层槽位。
输入校验
每次调用都会经过下面这组检查;任意一条不满足都会让本次调用直接失败,session 状态不会被修改:
todos字段必填且必须是数组。缺失字段或字面量null会被直接拒绝,不会被当作"清空全部"。要清空清单请显式发送{"todos": []}。- 每个条目必须包含非空的
content、非空的activeForm,以及合法的status(pending/in_progress/completed)。 - 同一时刻最多一个条目处于
in_progress。 content在清单内必须唯一(精确字符串匹配,不做 trim / 大小写归一 / Unicode 归一)。
风格性建议——例如"实际工作期间始终恰好一个 in_progress"、条目措辞、何时值得调用 todo_write 等——放在 todo.DefaultToolPrompt 里,不在上述检查中强制;因此你可以根据自身业务或所用模型族调整 prompt,不需要改工具。
自定义
基础工具的完整可运行示例(包含多轮暂停/续接场景与 ASCII 渲染器)见 examples/todo/。带强制完成对照的示例见 examples/todoenforcer/。
MCP Tools 协议工具
MCP(Model Context Protocol)是一个开放协议,标准化了应用程序向 LLM 提供上下文的方式。MCP 工具基于 JSON-RPC 2.0 协议,为 Agent 提供了与外部服务的标准化集成能力。
MCP ToolSet 特点:
- 🔗 统一接口:所有 MCP 工具都通过
mcp.NewMCPToolSet()创建 - 🚀 多种传输:支持 STDIO、SSE、Streamable HTTP 三种传输方式
- 🔧 工具过滤:支持包含/排除特定工具
- ✅ 显式初始化:通过
(*mcp.ToolSet).Init(ctx),可以在应用启动阶段提前发现 MCP 连接/工具加载错误并快速失败
基本用法
MCP Annotations 与权限 Metadata
当远端 MCP server 在 tools/list 中返回 tool annotations 时,直接通过
MCP ToolSet 暴露的工具会实现 tool.MetadataProvider。Permission policy
可以直接读取 req.Metadata.ReadOnly、req.Metadata.Destructive 和
req.Metadata.OpenWorld,无需再解析 MCP 专属结构。
这个映射基于 tools/list 返回的工具快照。ToolSet 刷新工具列表时,会
重新构造框架内的工具列表,而不是原地修改已有 mcpTool 实例。如果未来
支持基于 MCP ToolListChangedNotification 的原地热更新,需要重新评估
metadata 读取的线程安全性。
ToolSet 生命周期与所有权
ToolSet 接口里显式提供了 Close(),这意味着它持有的连接、会话和缓存
等资源需要由创建它的一方负责释放。
几个容易混淆的边界:
llmagent.WithToolSets(...)只是把ToolSet挂到 Agent 上使用, 不会转移其所有权。LLMAgent的AddToolSet(...)、RemoveToolSet(...)、SetToolSets(...)只会更新 Agent 当前暴露的工具集合, 不会自动调用旧ToolSet的Close()。runner.NewRunner(...)和runner.NewRunnerWithAgentFactory(...)也不会因为 Agent 使用了某个ToolSet,就在Runner.Close()时 自动回收它。
推荐的使用方式:
- 长生命周期 ToolSet:在应用启动时创建并可选执行
Init(ctx), 多次请求复用;应用退出时统一Close()。 - 按请求创建的 ToolSet:只在当前 run 内使用;当前 run 结束后由 调用方显式清理。
如果你只是希望 ToolSet 在每次执行时重新获取最新工具列表,优先使用
llmagent.WithRefreshToolSetsOnRun(true)。这会在每次 run 前重新调用
ToolSet.Tools(ctx),但不会为你重建或关闭 ToolSet 实例本身。
传输方式配置
MCP ToolSet 通过 Transport 字段支持三种传输方式:
1. STDIO 传输
通过标准输入输出与外部进程通信,适用于本地脚本和命令行工具。
2. SSE 传输
使用 Server-Sent Events 进行通信,支持实时数据推送和流式响应。
3. Streamable HTTP 传输
使用标准 HTTP 协议进行通信,支持普通 HTTP 和流式响应。
按请求 HTTP Header
对 SSE 和 Streamable HTTP 传输,可以使用上游 MCP 客户端提供的
mcp.WithHTTPBeforeRequest,在每一次 MCP HTTP 请求发出前,从当前调用
的 context 动态注入 header。一个长生命周期 MCP ToolSet 服务多个已登录
用户时,可以用它按用户注入 token、JWT 或业务签名。把 hook 通过
WithMCPOptions 传入即可:
ConnectionConfig.Headers 提供的静态 header 会先生效;之后每次 HTTP
请求都会走一遍 before-request hook,hook 内对同名 header 的 Set 会
覆盖静态值。
如果你是通过 llmagent.WithToolSets(...) 挂载这个 ToolSet,并且希望
initialize / tools/list 也能拿到请求级 header,建议同时配合
llmagent.WithRefreshToolSetsOnRun(true),或者先手动 toolSet.Tools(ctx),
再通过 llmagent.WithTools(...) 注入。
会话重连支持
MCP ToolSet 支持自动会话重连,当服务器重启或会话过期时自动恢复连接。
重连特性:
- 🔄 自动重连:检测到连接断开或会话过期时自动重建会话
- 🎯 独立重试:每次工具调用独立计数,不会因早期失败影响后续调用
- 🛡️ 保守策略:仅针对明确的连接/会话错误触发重连,避免配置错误导致的无限循环
MCP 工具的动态发现与更新(LLMAgent 配置项)
对于 MCP 工具集,服务器端的工具列表是可以变化的(例如在运行
过程中新增了一个 MCP 工具)。如果希望 LLMAgent 在每次调用
时自动看到最新的工具列表,可以在使用 WithToolSets 的同时,
开启 llmagent.WithRefreshToolSetsOnRun(true)。
LLMAgent 配置示例
当启用 WithRefreshToolSetsOnRun(true) 时:
- LLMAgent 在每次执行前构造工具列表时,会再次调用
ToolSet.Tools(ctx),其中ctx为本次执行的上下文; - 如果 MCP 服务器新增或删除了工具,该 Agent 下一次执行 时, 会自动使用更新后的工具列表。
- 如果你在“非执行期”获取工具(例如直接调用
agent.Tools()), LLMAgent 会使用context.Background()。
这个配置项的侧重点是动态发现工具。如果你还需要基于
context.Context 在初始化或工具发现阶段做更细粒度的控制,同时又不希望
在每次执行时刷新工具列表,可以参考 examples/mcptool/http_headers
示例,手动调用 toolSet.Tools(ctx),然后配合 WithTools 使用。
常见误区:
WithRefreshToolSetsOnRun(true)刷新的是工具列表,不是ToolSet实例本身;它不会自动新建、替换或关闭ToolSet。tools/call会使用本次 run 的上下文,但如果你在执行期外直接调用agent.Tools(),ToolSet 看到的是context.Background()。- 如果你需要让
initialize/tools/list也严格使用某个自定义上下文 (例如每次请求不同的认证头、追踪字段),更稳妥的做法通常是手动toolSet.Tools(ctx),再通过WithTools(...)注入。
MCP Broker(按需发现 MCP)
除了直接把远端 MCP 工具展开成一等 Tool 之外,框架还提供了另一种
接入方式:tool/mcpbroker。
mcpbroker 的核心思路是:
- 不在一开始把远端 MCP 的全部工具都暴露给模型
- 先只暴露少量 broker 工具
- 让模型在需要时再逐步发现和调用远端 MCP 能力
这类模式更适合长尾工具很多、但单次请求只会命中少量工具的场景。
什么时候用 MCP Broker
推荐使用 mcpbroker 的场景:
- 某个 MCP 服务工具很多,不希望每轮都把完整工具表面塞给模型
- 某些能力是“储备能力”或“长尾能力”,并不是高频调用
- 需要通过 Skill、System Prompt、User Prompt 等增量信息,动态连接某个远端 MCP endpoint
- 希望用较小、较稳定的初始工具面换取更低的上下文压力
更适合继续使用 mcp.NewMCPToolSet() 的场景:
- 高频、稳定、已知能力
- 希望把远端工具直接升格成一等 Tool
- 更看重调用路径更短、约束更强、工具调用成功率更高
这两种方式可以组合使用:
- 高频热点能力继续使用
MCP ToolSet - 低频长尾能力放进
mcpbroker
与 MCP ToolSet 的区别
两者的主要区别是“工具暴露时机”不同:
MCP ToolSet- 在初始化或运行时先
initialize + tools/list - 把远端每个 MCP tool 直接变成 Agent 可见 Tool
- 把远端 MCP 工具显式声明的安全 annotations 映射到
PermissionRequest.Metadata
- 在初始化或运行时先
mcpbroker- 初始只暴露 4 个 broker 工具
- 模型先发现 server,再发现 tool,再检查指定 tool 的 schema,最后再调用
- 暴露的是
mcp_call等 broker 工具,远端 tool annotations 不会自动进入PermissionRequest.Metadata
可以把它理解为:
MCP ToolSet:直接挂远端工具mcpbroker:按需发现远端工具
典型 trade-off:
MCP ToolSet:更快、更强约束,但工具面更大mcpbroker:更省上下文、更适合长尾与动态能力,但多一步 discovery,整体可能更慢
基本接入方式
Server Description(服务描述)
ConnectionConfig 上的 Description 字段为 MCP server 提供一段能力摘要,
帮助模型在 mcp_list_servers 阶段判断该去哪个 server 探索。
mcp_list_servers 的返回值会包含这个 description:
这类似于 OpenAI tool namespace 的 description:模型在第一步
mcp_list_servers 时就能根据描述决定"去哪个 server 看",而不需要
逐个 mcp_list_tools 试探。server 数量越多、名字越不直观时,
description 的价值越大。
这个字段是可选的。如果不填,输出中不会出现 description,对现有行为
没有任何影响。
当前 mcpbroker 是工具层接入,不会像 Skill 那样自动向
System Prompt 注入策略提示。如果你希望模型更稳定地理解:
- 什么时候先列 server
- 什么时候先
mcp_list_tools - 什么时候再
mcp_inspect_tools - 什么时候再
mcp_call
通常仍然建议在业务侧 instruction 里补少量高层 guidance。
模型可见的 4 个工具
当前模型只会看到这 4 个工具:
mcp_list_serversmcp_list_toolsmcp_inspect_toolsmcp_call
推荐的调用顺序通常是:
mcp_list_servers():查看 broker 已知的命名 MCP servermcp_list_tools(selector):查看某个 server 或远端 URL 的轻量工具目录mcp_inspect_tools(selector, tools[]):只展开指定工具的 schemamcp_call(selector, arguments):调用具体 MCP tool
这意味着模型不再一开始就看到完整远端工具集,而是先通过 broker 渐进式探索。
Selector 心智模型
mcpbroker 的核心输入不是 server_name + tool_name + url 的混合对象,
而是统一使用 selector:
- 在
mcp_list_tools中:- 命名 server:
local_stdio_code - 动态 URL:
https://example.com/mcp
- 命名 server:
- 在
mcp_call中:- 命名 tool:
local_stdio_code.add - 动态 URL tool:
https://example.com/mcp.add
- 命名 tool:
如果某个 ad-hoc HTTP endpoint 会让点分隔 selector 产生歧义,也支持:
https://example.com/mcp#tool=add
真正的 MCP tool 参数统一放在:
mcp_call(..., arguments={...})
而不是放在顶层字段里。
渐进式发现方式
- 先用
mcp_list_tools获取轻量摘要 - 只有准备调用某个具体 tool 时,再用
mcp_inspect_tools只展开该 tool 的 schema
这和“先把完整 schema 全部塞给模型”相比,更适合上下文预算紧张的场景。
动态 URL 与 Skill 场景
mcpbroker 支持 ad-hoc HTTP MCP:
WithAllowAdHocHTTP(true) 会让 HTTP(S) MCP 的 selector 成为模型可控输入。
生产环境里,建议先在业务侧对 URL、域名和路径做 allowlist 或其它校验,
再把 ad-hoc HTTP 当成可信集成路径使用。
这类动态连接通常需要先有一个“信息来源”告诉模型:
- 这个 MCP endpoint 存在
- 它大概能做什么
- 该用什么 URL 去连接
这个信息来源可以是:
- System Prompt
- User Prompt
- Skill
- 知识库
也就是说,mcpbroker 解决的是“如何连、如何看、如何调”,
而不是“模型为什么会想到去连这个 MCP”。
这也让 mcpbroker 很适合与 Skill 配合使用。有些 Skill 只在自身
场景下需要某个专用 MCP 能力,这类 MCP 工具不一定要在整个会话中
长期作为全局工具暴露;还有些 Skill 会在加载后提供一个增量出现的
远端 MCP endpoint。此时 Skill 可以负责提供“这个 MCP 存在、能做什么、
该连哪个 URL”的信息来源,而 mcpbroker 负责动态连接以及渐进式暴露
工具和 schema。
完整示例可参考:
examples/mcpbroker/basic
其中包含:
- 本地命名 MCP server
- Skill 提供远端 streamable HTTP MCP endpoint
- 模型通过
skill_load -> mcp_list_tools -> mcp_inspect_tools -> mcp_call动态连接远端 MCP
鉴权 Hook(Per-Run Header 注入)
对于 HTTP 型 MCP,mcpbroker 还提供两类运行时扩展点:
WithHTTPHeaderInjector(...)WithErrorInterceptor(...)
适用场景:
- 不希望让模型直接携带
Authorization - 需要根据当前用户、租户、workspace,在每次调用时动态注入 token
- 希望在远端返回 401/403 时,由业务层把底层错误包装成更友好的错误信息
示例:
这里的设计重点是:
- 模型只负责选择
selector - 业务代码负责从
ctx中识别当前用户,并注入 HTTP Header mcpbroker本身不管理复杂的 OAuth session 状态机
开启 WithAllowAdHocHTTP(true) 后,URL selector 可能来自模型可见上下文。
两类职责建议分开处理:
HTTPHeaderInjector只决定是否、向谁注入敏感 Header。可以基于req.IsAdHoc和req.BaseURL做粗粒度判断,例如对未知 BaseURL 直接返回nil,避免向不可信目标泄露 token。- 出站 URL allowlist / 域名校验等网络出站策略放到
WithClientOptionsProvider返回的tmcp.WithHTTPBeforeRequest里实现, 详见下一节。
完整示例可参考:
examples/mcpbroker/authhooks
底层 Client 选项(WithClientOptionsProvider)
除 Header 注入与错误拦截外,可用 WithClientOptionsProvider 在解析目标并完成 WithHTTPHeaderInjector 之后、创建底层 MCP 客户端之前,由业务返回 trpc-mcp-go 原生的 tmcp.ClientOption / tmcp.StdioClientOption。
- 顺序:默认的 HTTP Header(含注入结果)先应用,再追加 provider 返回的 HTTP options,便于在
WithHTTPBeforeRequest等钩子里做 URL 校验、审计或覆盖 Header。 ClientOptionsRequest:携带Selector、ServerName、Origin(mcpbroker.OriginCode/mcpbroker.OriginAdhoc)、TargetType、Phase(如mcpbroker.PhaseListTools)、ToolName(调用阶段)、以及本次调用的配置副本Config。- 返回值:
nil, nil表示不追加;返回error会在建连前失败(适合 URL policy 拒绝等契约)。 stdio:命名 stdio 目标时,向ClientOptions.Stdio传入选项即可(例如WithStdioCapabilities)。
框架不内置易误杀的 URL 全局拒绝列表;若需限制 ad-hoc 出站,可在 provider 里结合 tmcp.WithHTTPBeforeRequest 自行实现策略。
示例(仅说明形态,按业务调整):
Agent 工具 (AgentTool)
AgentTool 允许把一个现有的 Agent 以工具的形式暴露给上层 Agent 使用。相比普通函数工具,AgentTool 的优势在于:
- ✅ 复用:将复杂 Agent 能力作为标准工具复用
- 🌊 流式:可选择将子 Agent 的流式事件“内联”转发到父流程
- 🧭 控制:通过选项控制是否跳过工具后的总结补全、是否进行内部转发
基本用法
流式内部转发详解
当 WithStreamInner(true) 时,AgentTool 会把子 Agent 在运行时产生的事件直接转发到父流程的事件流中:
- 转发的事件本质是子 Agent 里的
event.Event,包含增量内容(choice.Delta.Content) - 为避免重复,子 Agent 在结束时产生的“完整大段内容”不会再次作为转发事件打印;但会被聚合到最终
tool.response的内容里,供下一次 LLM 调用作为工具消息使用 - UI 层建议:展示“转发的子 Agent 增量内容”,但默认不重复打印最终聚合的
tool.response内容(除非用于调试) - 通过
WithInnerTextMode(agenttool.InnerTextModeExclude),你可以保留内部 tool 进度,同时隐藏子 Agent 的 assistant 正文。这在外层协调者还会继续总结时尤其有用。
示例:仅在需要时显示工具片段,避免重复输出
工具结果响应模式
AgentTool 内部始终是以事件流的形式运行子 Agent。响应模式只控制 AgentTool 最终作为“工具结果”返回给父 Agent 的内容;它不会改变 子 Agent 的 session 写入、事件过滤键,也不会改变内部流式转发行为。
默认情况下,AgentTool 保持历史兼容行为:子 Agent 产生的每一条非空
assistant 消息都会被追加到同一个工具结果字符串里。这个行为对存量调用方最安全,
但长任务子 Agent 往往会输出进度、草稿或中间结论,这些中间 assistant 内容也会
进入父 Agent 看到的 tool.response。
当子 Agent 的目标是“隔离上下文、独立完成任务、只把最终答案交给父 Agent”
时,可以使用 WithResponseMode(agenttool.ResponseModeFinalOnly):
在 final-only 模式下,AgentTool 会:
- 忽略 partial assistant 增量
- 忽略非 assistant 消息、空 assistant 消息以及 tool 消息
- 返回子 Agent 最后一条完整 assistant 消息
- 如果子 Agent 结束时没有完整 assistant 消息,则返回空字符串
- 子 Agent 报错时仍返回
agent error: ...
它和 WithSkipSummarization(true) 不是一回事。WithSkipSummarization
控制的是父流程拿到工具响应后,是否再调用一次外层模型做总结;
WithResponseMode 控制的是工具响应本身应该包含哪些子 Agent 内容。
组合示例:
子 Agent 上下文可见性
AgentTool 会复用当前 invocation 和 session 来运行子 Agent。为了避免把几个 相近概念混在一起,可以先按下面的职责理解:
| 概念 | 作用 | 不负责什么 |
|---|---|---|
FilterKey |
标记事件属于哪个会话视图;内容处理器用它决定哪些历史消息会进入模型请求 | 不是权限边界,也不是独立存储 |
Branch |
记录 Agent 执行链路,主要用于 trace、跨 Agent 消息投影等场景 | 一般不直接决定 AgentTool 能读哪些历史 |
HistoryScope |
AgentTool 生成子 Agent FilterKey 的策略 |
不改变工具结果如何裁剪,也不改变子 Agent 可用工具 |
MessageFilterMode |
LLMAgent/GraphAgent 的高层历史过滤预设,组合了时间维度和 FilterKey 维度 |
普通 AgentTool 使用者通常不需要直接配置 |
由于历史命名原因,BranchFilterMode 这个名字里有 Branch,但对当前版本事件,
它比较的是 Event.FilterKey 与当前 invocation 的 FilterKey。在 FilterKey
出现前写入的旧版本事件,为了兼容仍可能回退使用 Event.Branch。
AgentTool 目前有两个历史作用域:
HistoryScopeIsolated(默认):子 Agent 使用独立的FilterKey,例如math-specialist-<uuid>(每次调用都会生成新的子 key)。在正常 Runner 生成的事件下, 子 Agent 只会看到本次工具调用参数,不会继承父 Agent 的历史。子 Agent 的事件仍会写入 同一个 session,但位于独立视图下。- 如果你希望“同一个 AgentTool 多次调用时,子 Agent 能看到自己上一次的执行历史”,可以
在
HistoryScopeIsolated下开启WithPersistentHistory*(见下文选项说明)。此时子 key 会改为稳定值(例如agenttool:math-specialist:default),从而让子 Agent 的上下文连续。
- 如果你希望“同一个 AgentTool 多次调用时,子 Agent 能看到自己上一次的执行历史”,可以
在
HistoryScopeParentBranch:子 Agent 使用父 key 的子 key,例如assistant/math-specialist-<uuid>。默认的 prefix 匹配会把祖先和子孙都视为同一 上下文链路,因此子 Agent 可以看到父 Agent 历史,父 Agent 后续也可能看到这个 子 Agent 的事件。这个模式适合共享上下文的协作链路,不是“只读取父历史但不污染 父历史”的快照隔离。
换句话说:
- 想让子 Agent 做独立工作,只把最终答案作为工具结果交回父 Agent:使用默认
HistoryScopeIsolated,必要时把上下文显式放进工具参数。 - 想让子 Agent 基于父 Agent 的历史继续编辑、优化或续写,并接受父子事件处在同一
上下文链路中:使用
HistoryScopeParentBranch。
HistoryScope 只控制历史可见性。下面这些行为不会因为切换 HistoryScope 而改变:
WithResponseMode仍然只控制工具结果里返回哪些子 Agent assistant 内容。WithSkipSummarization仍然只控制父流程是否在工具结果后追加一次外层总结调用。- 子 Agent 仍通过
Invocation.Clone(...)继承当前 invocation 的 session、plugins、RunOptions等运行上下文;如果需要真正后台隔离,请启动独立的应用运行流程。 - 如果业务手动追加了空
FilterKey事件,这类事件按兼容规则可能被多个视图看到; 自定义事件建议始终设置带 app 前缀的明确FilterKey。
选项说明
-
WithSkipSummarization(bool):
- false(默认):允许在工具结果后继续一次 LLM 调用进行总结/回答
- true:外层 Flow 在
tool.response后直接结束本轮(不再额外总结)
-
WithStreamInner(bool):
- true:把子 Agent 的事件直接转发到父流程(强烈建议父/子 Agent 都开启
GenerationConfig{Stream: true}) - false:按“仅可调用工具”处理,不做内部事件转发
- true:把子 Agent 的事件直接转发到父流程(强烈建议父/子 Agent 都开启
-
WithInnerTextMode(InnerTextMode):
InnerTextModeInclude:实际默认行为,开启内部转发时继续展示子 Agent 的 assistant 文本InnerTextModeExclude:隐藏子 Agent 的 assistant 文本,但继续保留内部 tool call、tool.done,以及聚合后的最终工具响应
-
WithResponseMode(ResponseMode):
ResponseModeDefault(默认):保持历史兼容行为,把子 Agent 的 assistant 消息拼接成工具结果ResponseModeFinalOnly:只把子 Agent 最后一条完整 assistant 消息作为工具结果返回
-
WithPersistentHistory() / WithPersistentHistoryKey(string) / WithPersistentHistoryKeyFunc(...):
- 作用:在
HistoryScopeIsolated下使用稳定的子FilterKey,让子 Agent 能在同一个 session 内跨多次 AgentTool 调用读取自己的历史(而不是每次都从“全新子 key”开始)。 - 默认 key:
agenttool:<toolName>:default(刻意不含/,避免和父 key 形成前缀关系导致父侧误入上下文)。 - 高级用法:用
WithPersistentHistoryKey(...)或WithPersistentHistoryKeyFunc(...)把不同任务分到不同 key, 防止多个并发/多任务把历史混在一起。 - 注意:
- 这不是权限边界;如果父 Agent 侧配置了
BranchFilterModeAll/ 空 key,或你把子 key 设计成parent/...这类 前缀关系,父 Agent 仍可能在后续请求里看到子事件。 - 子 Agent 是否真的能看到“更早历史”,还取决于子 Agent 自身的
MessageFilterMode/ timeline 过滤(默认会包含历史; 如果你把子 Agent 配成只看当前 request/invocation,则稳定 key 也不会带来跨次可见)。 - 目前仅
agenttool.NewTool(agent)支持;agenttool.NewDynamicTool()会忽略该配置(dynamic 设计上是短生命周期、无记忆的)。 - 与
HistoryScopeParentBranch不兼容:当同时开启时,框架会忽略 persistent history,并沿用parent/child-uuid语义。
- 这不是权限边界;如果父 Agent 侧配置了
- 完整示例:见
examples/agenttool/(通过-persistent-child-history/-persistent-child-key体验)。
- 作用:在
- WithHistoryScope(HistoryScope):
HistoryScopeIsolated(默认):子调用使用独立FilterKey,通常只读取本次工具参数,不继承父历史。HistoryScopeParentBranch:子调用使用父键/子名-UUID(Universally Unique Identifier,通用唯一识别码)形式的分层FilterKey。父子事件处于同一上下文链路,prefix 匹配下可互相进入上下文。典型场景:基于上一轮产出进行“编辑/优化/续写”。
示例:
注意事项
- 事件完成信号:工具响应事件会被标记
RequiresCompletion=true,Runner 会自动发送完成信号,无需手工处理 - 内容去重:如果已转发子 Agent 的增量内容,默认不要再把最终
tool.response的聚合内容打印出来 - “只看进度”体验:当你希望用户看到内部进度、但不想重复看到子 Agent 正文时,可组合使用
WithStreamInner(true)和WithInnerTextMode(agenttool.InnerTextModeExclude) - 模型兼容性:一些模型要求工具调用后必须跟随工具消息,AgentTool 已自动填充聚合后的工具内容满足此要求
- 子 Agent 上下文隔离和工具结果裁剪是两件事:
WithHistoryScope(agenttool.HistoryScopeIsolated)控制子 Agent 能读到什么;WithResponseMode(agenttool.ResponseModeFinalOnly)控制父 Agent 作为工具结果收到什么。 HistoryScopeParentBranch是共享上下文链路,不是快照隔离。如果不希望子 Agent 的详细事件在父 Agent 后续上下文中出现,保持默认HistoryScopeIsolated,并把必要上下文放进工具参数。WithSkipSummarization(true)只会跳过额外的外层总结型 LLM 调用,不会把tool.response变成 assistant final response;如果你需要真正的终止信号,仍应持续消费到runner.completion
动态 AgentTool
agenttool.NewTool(agent) 适合工具背后已经有一个明确的专家 Agent:开发者先把
Agent、模型、工具、skills、权限等配置好,再把它包装成父 Agent 的一个工具。
当你的应用无法提前穷举所有专家角色,而是希望父 Agent 在每次调用时按任务临时选择
工具子集或指定本次执行指令,可以使用 agenttool.NewDynamicTool()。它会向模型暴露
一个默认名为 dynamic_agent 的工具;模型调用它时,不是在创建任意 Go 对象,也不是
选择某个已注册 Agent,而是在代码定义的边界内运行一次短生命周期的子 Agent invocation。
典型接入方式如下:
默认情况下,dynamic_agent 的能力边界来自父 Agent 当前可用的业务工具。模型可以通过
tools 字段把子 Agent 本次可用工具收窄到其中一部分;如果不传 tools,则允许使用
边界内的全部工具。dynamic_agent 自身、transfer_to_agent 以及调用方执行的外部工具
不会进入这个子 Agent 的可选工具面。
模型侧默认可见的参数包括:
request:必填,描述子 Agent 本次要完成的任务。默认历史作用域是HistoryScopeIsolated,因此建议把完成任务需要的上下文写进request。instruction:可选,作为本次子 Agent invocation 的角色、约束或执行指令。tools:可选,精确指定本次允许子 Agent 使用哪些工具名。传空数组表示本次不授予 任何业务工具。
如果默认从父 Agent 派生能力面不符合业务边界,可以在代码侧显式设置模板 Agent 或最大 能力面:
WithTemplateAgent 是代码侧边界,不是模型参数。模型不能通过 dynamic_agent 选择任意
Agent、模型或 executor;它只能在开发者配置好的边界内,为这一次调用填写 request、
instruction,并按需选择 tools/skills 子集。
常用选项:
WithName(name):修改模型可见工具名。仅对NewDynamicTool生效;普通NewTool(agent)的工具名始终来自被包装 Agent 的Info().Name。WithTemplateAgent(agent):设置动态子 Agent 的模板,常用于固定模型、executor、 callbacks、权限策略等执行边界。WithCapabilityTools(tools):设置模型可选择的最大工具集合。未设置时默认从父 Agent 本轮有效业务工具派生。显式设置后,这些工具名会被枚举进tools字段的 schema,模型从 已知集合中选择而非猜测字符串(父派生工具面与WithCapabilityProvider在每次调用时 解析,不会在此枚举)。WithCapabilitySkills(repo):设置模型可选择的最大 skill 仓库。未设置时默认从父 Agent 本轮有效 skill 仓库派生。WithExposeToolSelection(false):不向模型暴露tools字段。子 Agent 仍使用代码边界内 的工具面,但模型不能进一步收窄。WithExposeSkillSelection(true):向模型暴露skills字段。默认关闭,因为 skill 是否 可执行通常依赖部署环境和 code executor。WithExposeInstruction(false):不向模型暴露instruction字段。WithRequestDescription/WithInstructionDescription/WithToolsDescription/WithSkillsDescription:按业务语义调整字段描述,帮助模型更 稳定地填写参数。
使用 skills 时需要额外注意 executor 环境。Dynamic AgentTool 会校验模型选择的 skill
是否在边界仓库中;如果子 Agent 没有可用 code executor,而选中的 skill 可能需要执行代码,
工具结果会带上提示。生产环境中更推荐在代码侧通过 WithCapabilitySkills 和模板 Agent
先定义清楚可执行范围,再决定是否把 skills 字段暴露给模型。
Dynamic AgentTool 与另外两种多 Agent 机制的边界不同:
| 机制 | 模型选择什么 | 生命周期 | 控制权 |
|---|---|---|---|
agenttool.NewTool(agent) |
一个固定的工具入口 | 每次工具调用 | 返回工具结果给父 Agent |
transfer_to_agent |
一个已注册 sub-agent | 当前轮继续由目标 Agent 处理 | 控制权移交 |
agenttool.NewDynamicTool() |
本次调用的 request、instruction 和 tools/skills 子集 |
每次工具调用 | 返回工具结果给父 Agent |
如果同一个专家 Agent 同时通过 WithSubAgents 和 agenttool.NewTool(agent) 暴露给父
Agent,模型会看到两条不同路径:transfer_to_agent 和普通 AgentTool。框架可以运行,
但开发者应在 instruction 或工具 description 中明确何时使用哪一种,或者只保留一种入口。
dynamic_agent 子调用内部不会获得 transfer_to_agent,但普通 AgentTool 会被视作业务工具;
如果不希望动态子 Agent 再调用其他 AgentTool,可以用 WithCapabilityTools 或运行时
ToolFilter 收窄边界。
工具集成与使用
创建 Agent 与工具集成
MCP 工具过滤器
MCP 工具集支持在创建时过滤工具。推荐使用统一的 tool.FilterFunc 接口:
运行时工具过滤
- 方式一:运行时工具过滤允许在每次
runner.Run调用时动态控制工具可用性,无需修改 Agent 配置。这是一个"软约束"机制,用于优化 token 消耗和实现基于角色的工具访问控制。针对所有agent生效 - 方式二:通过
llmagent.WithToolFilter配置运行时过滤function, 只对当前agent生效
核心特性:
- 🎯 Per-Run 控制:每次调用独立配置,不影响 Agent 定义
- 💰 成本优化:减少发送给 LLM 的工具描述,降低 token 消耗
- 🛡️ 智能保护:框架工具(
transfer_to_agent、knowledge_search、agentic_knowledge_search、可选的await_user_reply)自动保留,永不被过滤 - 🔧 灵活定制:支持内置过滤器和自定义 FilterFunc
Tool Search(自动工具筛选)
除了“规则过滤(Tool Filter)”,框架还提供 Tool Search:在每次主模型调用前,先做一次“工具选择”,把候选工具集压缩到 TopK(例如 3 个),再交给主模型执行,从而进一步降低 token(尤其是 PromptTokens)。
需要注意的 trade-off:
- 耗时:Tool Search 会引入额外步骤(额外 LLM 调用、以及/或 embedding + 向量检索),端到端耗时可能增加。
- Prompt Caching:每轮传给主模型的工具列表会变化,可能降低部分平台的 prompt caching 命中率。
和 Tool Filter 的区别:
- Tool Filter:你(或业务)通过规则决定“允许/禁止哪些工具”(访问控制/成本控制),更偏策略与安全。
- Tool Search:框架根据“当前用户问题”自动挑选相关工具,更偏自动化与成本优化。
它们可以组合使用:先用 Tool Filter 做权限/白名单,再用 Tool Search 在剩余工具里做 TopK 选择。
两种策略:
- LLM Search:把候选工具列表(name + description)拼进 prompt,让 LLM 直接输出“应该使用哪些工具”。
- 优点:不依赖向量库;实现简单。
- 缺点:每轮都会把工具列表放进 prompt,开销随工具数量/描述长度近似线性增长。
- Knowledge Search:先用 LLM 做 query rewrite,再用 embedding + 向量检索做语义匹配。
- 优点:不需要每轮把完整工具列表塞进 LLM;并且 tool embedding 会在同一
ToolKnowledge实例内缓存,后续轮/后续请求可以复用。 - 注意:每轮仍需要对 query 做 embedding(固定开销之一)。
- 优点:不需要每轮把完整工具列表塞进 LLM;并且 tool embedding 会在同一
基本用法(LLM Search)
Tool Search 既可以作为 Runner plugin 使用,也可以作为单个 Agent 的 callback 使用。
方案 A:Runner Plugin
方案 B:Per-Agent BeforeModel Callback
通过 modelCallbacks.RegisterBeforeModel(...) 注册 Tool Search 的 callback
(会在主模型调用前自动重写 req.Tools):
基本用法(Knowledge Search)
需要先创建 ToolKnowledge(embedding + vector store),再通过 toolsearch.WithToolKnowledge(...) 启用 Knowledge Search:
Token 统计(可选)
Tool Search 的 token usage 会写入 context,可用于打点与成本分析:
基本用法
1. 排除特定工具(Exclude Filter)
使用黑名单方式排除不需要的工具:
2. 只允许特定工具(Include Filter)
使用白名单方式只允许指定的工具:
3. 自定义过滤逻辑(Custom FilterFunc)
实现自定义过滤函数以支持复杂的过滤逻辑:
4. Agent 粒度过滤(Per-Agent Filtering)
通过 agent.InvocationFromContext 实现不同 Agent 使用不同工具:
完整示例: 参见 examples/toolfilter/ 目录
智能过滤机制
框架会自动区分用户工具和框架工具,只过滤用户工具:
| 工具分类 | 包含的工具 | 是否被过滤 |
|---|---|---|
| 用户工具 | 通过 WithTools 注册的工具通过 WithToolSets 注册的工具 |
✅ 受过滤控制 |
| 框架工具 | transfer_to_agent(多 Agent 协调)knowledge_search(知识库检索)agentic_knowledge_searchawait_user_reply(开启后的一次性追问路由) |
❌ 永不过滤,自动保留 |
示例:
用 await_user_reply 处理跨轮追问
await_user_reply 是一个可选框架工具。当某个 Agent 可能向用户补问信息,并且
你希望下一条用户消息继续回到这个 Agent 时,可以开启
llmagent.WithAwaitUserReplyTool(true)。
它需要和 runner.WithAwaitUserReplyRouting(true) 搭配使用:
这条路由是一次性的:Runner 会在下一条用户消息到来时消费它,然后自动清掉。
注意事项
⚠️ 安全提示: 运行时工具过滤是"软约束",主要用于优化和用户体验。工具内部仍需实现自己的鉴权逻辑:
手动执行工具(中断 tool_calls)
默认情况下,当模型返回 tool_calls 时,框架会自动执行工具,然后把工具结果再发回给模型继续推理。
在一些系统里,你可能希望由调用方(例如客户端、上游服务,或外部工具运行时,例如
Model Context Protocol (MCP))来执行工具。此时可以使用
agent.WithExternalTools(...) 或 agent.WithToolExecutionFilter(...)
来中断工具的自动执行。
核心区别:
agent.WithToolFilter(...)控制工具可见性(模型能看到/能调用哪些工具)agent.WithToolExecutionFilter(...)控制工具执行(模型请求后,框架是否自动执行)agent.WithAdditionalTools(...)为本次运行追加临时可见工具agent.WithExternalTools(...)追加临时可见工具,并声明这些工具由调用方执行
基本流程
- 使用
WithExternalTools发起一次runner.Run,让模型看到调用方工具 - 从事件里读取模型返回的
tool_calls - 调用方在外部执行工具
- 通过
role=tool的消息把结果回填,模型继续输出最终答案
如果工具已经通过 llmagent.WithTools(...) 注册在 Agent 上,只是想在某次
运行中改成由调用方执行,可以继续使用 agent.WithToolExecutionFilter(...)。
WithExternalTools 更适合 AG-UI、浏览器、移动端或上游服务在每次请求中动态声明
工具的场景。AG-UI runner 默认会把请求里的 input.Tools 映射为
WithExternalTools。外部工具与已有工具同名时,已有工具优先,外部声明不会覆盖或拦截它。这里的已有工具包括 Agent 上注册的工具,以及通过 WithAdditionalTools 追加的工具。
完整示例: examples/toolinterrupt/
原因: LLM 可能从上下文或记忆中知道工具的存在和用法,并尝试调用。工具过滤减少了这种可能性,但不能完全防止。
并行工具执行
Graph 工作流下也可以在工具节点开启并行:
并行执行效果:
运行时 ToolSet 动态管理
WithToolSets 是一种静态配置方式:在创建 Agent 时一次性注入 ToolSet。很多实际场景下,你希望在运行时动态增删 ToolSet,而不必重建 Agent。
LLMAgent 提供了三个与 ToolSet 相关的运行时方法:
AddToolSet(toolSet tool.ToolSet)—— 按ToolSet.Name()添加或替换同名 ToolSetRemoveToolSet(name string) bool—— 按名称移除所有同名 ToolSet,返回是否确实删除SetToolSets(toolSets []tool.ToolSet)—— 以给定切片整体替换当前所有 ToolSet
这些方法是并发安全的,并会自动重新计算:
- 聚合后的工具列表(显式
WithTools工具 + ToolSet 工具 + 知识检索工具 + Skills 工具) - “用户工具”跟踪信息(用于前文介绍的智能过滤机制)
需要特别注意:
AddToolSet替换同名 ToolSet 时,不会自动Close()被替换掉的旧实例。RemoveToolSet删除 ToolSet 时,不会自动Close()被移除的实例。SetToolSets整体替换时,不会自动Close()旧切片里的实例。
如果这些 ToolSet 是由你创建的,你仍然需要在合适的时机显式回收它们。
典型使用方式:
运行时 ToolSet 更新会自动与前文的工具过滤机制协同工作:
- 通过
WithTools和所有 ToolSet(包括动态添加的 ToolSet)注册的工具都视为用户工具,会受到WithToolFilter以及每次调用的运行时过滤控制。 - 框架工具(
transfer_to_agent、knowledge_search、agentic_knowledge_search、可选的await_user_reply)仍然 永远不被过滤,始终对 Agent 可用。
Tool Call 参数自动修复
部分模型在生成 tool_calls 时,可能产出非严格 JSON 的参数(例如对象 key 未加引号、尾逗号等),从而导致工具执行或外部解析失败。
Tool Call 参数自动修复功能适用于调用方需要在框架外部解析 toolCall.Function.Arguments,或工具严格要求入参为合法 JSON 的场景。
在 runner.Run 中启用 agent.WithToolCallArgumentsJSONRepairEnabled(true) 后,框架会尽力修复 toolCall.Function.Arguments。
快速开始
环境准备
简单示例
运行示例
总结
Tool 工具系统为 tRPC-Agent-Go 提供了丰富的扩展能力,支持函数工具、DuckDuckGo 搜索工具和 MCP 协议工具。