news 2026/5/28 23:40:06

工具调用:Agent 的手和眼

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
工具调用:Agent 的手和眼

系列「企业级 AI Agent 实现拆解」第七篇。上一篇讲了 Hook 系统,这篇看工具调用的完整设计。


工具是什么

LLM 本质上只能做一件事:根据输入文本预测输出文本。它没有手,没有眼,没办法查数据库、发邮件、调接口。工具调用(Tool Use)给了 Agent 这些能力——LLM 输出一个结构化的"我想调这个工具、传这些参数",系统真正执行,把结果塞回给 LLM,LLM 再决定下一步。

工具注册表(tool-broker BC)是这个系统的中心:所有工具都在这里注册、管理、调度。Agent 不直接知道工具的实现细节,只通过 gRPC 调ToolBroker.Invoke()

一次工具调用的完整流程

LLM 输出 ToolCall(工具名 + 参数 JSON) │ ▼ Eino 的 tools 节点 → toolBrokerAdapter.InvokableRun() │ 从 context 取 sessionID + tenantID │ ▼ gRPC 调用 tool-broker 的 InvokeHandler.Handle() │ ├── 1. 查工具:LoadByName(租户私有优先,找不到查全局) ├── 2. 选 runner:runners[tool.Impl()] → builtin/wasm/http/mcp ├── 3. 超时控制:tool.TimeoutMs() > 0 时设 context timeout ├── 4. 执行:runner.Run(ctx, tool, args) → 实际调用 ├── 5. 记审计:invLog.Append(Invocation{...}) → tool_invocations 表 │ 审计失败不影响结果返回,但会发 outbox 事件告警 └── 6. 返回结果 + latency

工具的两个核心属性

注册一个工具时,有两个属性最重要:

**Impl(实现类型)**决定工具怎么执行:

typeImplstringconst(ImplBuiltin Impl="builtin"// 同进程 Go 函数(kb.search、memory.set)ImplWASM Impl="wasm"// 租户上传 WASM,gVisor 沙箱隔离ImplHTTP Impl="http"// 远程 HTTP API,走 Connector 管 secretImplMCP Impl="mcp"// MCP 协议工具)

**Danger(危险等级)**决定是否触发 PreToolUse hook:

typeDangerstringconst(DangerSafe Danger="safe"// 纯读,不需审批(kb.search)DangerCaution Danger="caution"// 写但可回滚(memory.set)DangerHigh Danger="high"// 不可逆/对外副作用(sql.exec、email.send))

DangerHigh的工具,PreToolUse hook 会评估是否需要 HITL。评估逻辑写在 hook 配置里,不写在工具代码里——不同租户对"高危"的定义可以不同。


工具名必须带命名空间

funcNewTool(tenantID,name,descstring,...)(*Tool,error){if!strings.Contains(name,"."){returnnil,errors.New("name must use namespace.method format (e.g. kb.search)")}...}

工具名强制namespace.method格式,原因有两个:

  1. 避免命名冲突——不同租户的自定义工具和平台内置工具混在一起,命名空间做天然隔离
  2. PreToolUse hook 的 matcher 支持通配符——{"tool_name": "sql.*"}能匹配所有 SQL 类工具,不需要为每个工具单独写规则

调用记录:每次都留档

每次工具调用都写一条Invocation记录:

typeInvocationstruct{IDstringToolNamestringTenantIDstringSessionIDstringCallIDstring// 关联 agent ToolCall.IDArgsstring// JSONResultstring// JSON,>16k 走 MinIO + urlErrorstringLatencyMsint64StartedAt time.Time EndedAt time.Time}

CallID关联 LLM 输出的ToolCall.ID,审计时能从"这次 session 里某个工具调用"直接跳转到 LLM 当时给出的完整 tool_call。结果超 16KB 存 MinIO,表里只存 URL。


实战案例:两个免费 builtin 工具

光说模型可能还是抽象,看两个实际注册的 builtin 工具。它们都不需要密钥、不花钱,适合当入门案例。

案例一:web.wikipedia— 搜索维基百科

这是一个 safe 级别的只读工具。LLM 想查一个知识性问题(比如"量子计算的基本原理"),调这个工具拿维基百科的摘要。

调用方式:Agent 传{"query": "quantum computing"},工具返回匹配的条目列表(标题、URL、摘要)。

内部逻辑是两步走:

第 1 步:调 Wikipedia search API,拿到标题列表(TopK=3,默认返回 3 条) 第 2 步:对每个标题调 page API,拿正文摘要(截断到 2000 字符)

为什么分两步?因为 Wikipedia 的搜索 API 只返回标题和摘要片段,不返回完整正文。先搜到标题,再逐个取详情,是最稳的做法。

关键代码(简化):

funcBuildWikipediaSearch(cfg WikipediaConfig)HandlerFn{returnfunc(ctx context.Context,rawstring)(string,error){// 1. 解析入参varreqstruct{Querystring`json:"query"`}json.Unmarshal([]byte(raw),&req)// 2. 搜索:拿标题列表titles,_:=search(ctx,req.Query)// 3. 逐个取详情results:=[]wikipediaResult{}for_,title:=rangetitles{extract,url,_:=getPage(ctx,title)results=append(results,wikipediaResult{Title:title,URL:url,Extract:extract})}// 4. 返回 JSONout,_:=json.Marshal(map[string]any{"results":results})returnstring(out),nil}}

设计亮点:

  • WikipediaConfig有合理默认值(语言 en、TopK 3、超时 15s、截断 2000 字符),零值配置就能用
  • 纯标准库net/http,没有外部依赖
  • 结果截断到DocMaxChars,防止超长摘要吃掉 LLM 的上下文窗口

案例二:osm.geocode— 地理编码

这也是一个 safe 级别的只读工具。输入地名(比如"北京天安门"),返回经纬度、完整地址、类型、重要性评分。

调用方式:Agent 传{"query": "天安门广场"},工具返回匹配的地理信息列表。

关键代码(简化):

funcBuildOSMGeocode(cfg OSMGeocodeConfig)HandlerFn{returnfunc(ctx context.Context,rawstring)(string,error){// 1. 解析入参varreqstruct{Querystring`json:"query"`Limitint`json:"limit,omitempty"`}json.Unmarshal([]byte(raw),&req)// 2. 限制结果数量(防滥用)limit:=req.Limitiflimit<=0{limit=5}// 默认 5 条iflimit>conf.MaxLimit{limit=10}// 最多 10 条// 3. 调 Nominatim API(单次请求)// GET https://nominatim.openstreetmap.org/search?q=...&format=json&limit=5// 4. 解析返回,输出标准化结构out,_:=json.Marshal(map[string]any{"results":results})returnstring(out),nil}}

设计亮点:

  • 遵守 Nominatim 使用政策:User-Agent 必填、不并发批量请求
  • 结果数量有上限保护(MaxLimit=10),防 Agent 无意中请求太多数据
  • Accept-Language默认中文优先,对中文用户更友好

两个案例的共性

特性web.wikipediaosm.geocode
Dangersafesafe
外部依赖无(纯 stdlib)无(纯 stdlib)
密钥不需要不需要
超时15s(可配置)10s(可配置)
结果截断DocMaxChars=2000MaxLimit=10

两个工具都是只读的(safe),不需要密钥,纯标准库实现,配置有合理默认值。它们展示了 builtin 工具的典型模式:接收 JSON 参数 → 调外部 API → 返回 JSON 结果。写一个新的 builtin 工具,照这个模板写就行。


Builtin 工具一览

tool-broker 启动时通过RegisterStdBuiltins注册内置工具。注册哪些取决于环境变量配置——没配的就不注册,调用时返回 “no handler”(fail-closed 默认安全):

// register.go — 内置工具注册funcRegisterStdBuiltins(r*BuiltinRunner,d StdDeps)[]string{ifd.SQL!=nil{r.Register("sql.exec",BuildSQLExec(d.SQL))// high · 执行 SQL}ifd.FS!=nil{r.Register("file.read",BuildFileRead(d.FS,...))// safe · 读文件}ifd.Shell!=nil{r.Register("shell.run",BuildShellRun(d.Shell,...))// high · 执行 Shell}ifd.Calendar!=nil{r.Register("calendar.list",BuildCalendarList(...))// safe · 日历查询}ifd.Slack!=nil{r.Register("slack.send",BuildSlackSend(...))// caution · 发 Slack 消息}}
工具名危险等级说明
sql.exechigh执行 SQL(需审批)
file.readsafe读文件(可配置白名单目录)
shell.runhigh执行 Shell 命令(可配置白名单命令)
calendar.listsafe日历查询
slack.sendcaution发 Slack 消息(可配置白名单频道)
web.wikipediasafe维基百科全文搜索(免费,不需密钥)
osm.geocodesafeOpenStreetMap 地理编码(免费,不需密钥)

sql.execshell.runDangerHigh,每次调用都会触发 PreToolUse hook 评估。每个内置工具都支持配置限制参数(白名单目录、白名单命令、最大长度等),防止工具被滥用。

kb.searchmemory.set/get等工具由各自的 BC(知识库、记忆)注册,不走 tool-broker 的 builtin runner。


跟 Eino 的关系

Eino 定义了tool.BaseTool接口:Info()(返回工具描述和参数 schema)和InvokableRun()(执行)。infrastructure/einoadapter/tool.go把 tool-broker 的 gRPC 调用包成 EinoBaseTool,让 Eino 的 ReAct 图能直接调度:

// tool.go — toolBrokerAdapterfunc(t*toolBrokerAdapter)InvokableRun(ctx context.Context,argsstring,...)(string,error){// 从 context 取 sessionID(优先于 struct 字段)// 原因:Runnable 按 AgentConfig 缓存,跨 session 复用// struct 里的 sessionID 可能是第一次创建时的旧值sessionID,_:=ctx.Value(port.ContextKeyAgentSessionID{}).(string)ifsessionID==""{sessionID=t.sessionID// fallback}returnt.broker.Invoke(ctx,t.tenantID,sessionID,model.ToolCall{Name:t.schema.Name,Arguments:args,})}

注意 sessionID 的取法:先从 context 取,取不到才用 struct 字段。这是因为 Runnable(包含工具适配器的 Eino 图)按 AgentConfig 缓存,同一个图可能被多个 session 复用。sessionID 必须从每次请求的 context 里动态读取,不能绑在 struct 上。

Eino 负责工具的编排(按 LLM 输出决定调哪个),tool-broker 负责工具的执行(沙箱隔离、审计记录、Hook 链)。两者各管一块。


小结

工具调用这层设计的重点在四个地方:

  1. Danger 三态:在注册阶段就标记危险等级(safe/caution/high),hook 系统按等级决策
  2. Impl 四态:builtin/wasm/http/mcp 统一Runner接口(Kind()+Run()),agent 不知道执行细节
  3. Invocation 记录:每次调用留档,CallID串联 LLM 决策和实际执行,审计失败不阻塞结果返回
  4. 环境驱动注册:内置工具按环境变量决定是否注册,没配就不注册(fail-closed)

下一篇:多 LLM Provider —— 不改一行业务代码换模型

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 23:30:42

焕新不止于外观 专业服务全面升级

历经精心规划与深度打磨&#xff0c;新版官网正式亮相。我们跳出单纯的界面更新&#xff0c;结合行业趋势与用户反馈&#xff0c;对网站架构、视觉呈现、运行性能进行全方位重塑。全新的页面设计兼顾美感与实用性&#xff0c;操作逻辑清晰易懂&#xff0c;电脑端、移动端均可流…

作者头像 李华
网站建设 2026/5/28 23:30:40

CCX详细配置对接deepseek和Codex步骤

要将 DeepSeek 接入 OpenAI Codex&#xff0c;核心在于解决两者之间的协议不兼容问题&#xff1a;Codex 原生使用的是 OpenAI 的 Responses API 协议&#xff0c;而 DeepSeek 官方兼容的是 Chat Completions API。因此&#xff0c;我们需要借助 CCX 作为协议转换网关&#xff0…

作者头像 李华
网站建设 2026/5/28 23:30:23

卖覆铜板怎么找客户?PCB 厂的产业带分布与名单开发逻辑

卖覆铜板找客户&#xff0c;本质是找用覆铜板做基材的 PCB 制造厂。核心难点不在于产品本身&#xff0c;而在于把全国那些真实在产、真实消耗覆铜板的 PCB 厂名单拿到手——覆铜板的下游高度行业集中&#xff0c;但 PCB 厂分布广、细分类型多&#xff0c;不把下游版图梳理清楚&…

作者头像 李华
网站建设 2026/5/28 23:15:02

装配式篷房源头厂家哪家好

在当今快节奏的商业和工业领域&#xff0c;装配式篷房以其快速搭建、灵活使用等优势&#xff0c;成为了众多企业和活动的首选。然而&#xff0c;市场上的篷房厂家众多&#xff0c;质量和服务参差不齐&#xff0c;让许多用户在选择时感到困惑。今天&#xff0c;我们就来深入探讨…

作者头像 李华
网站建设 2026/5/28 23:11:29

抖音弹幕监听终极指南:基于系统代理技术的直播数据抓取实战教程

抖音弹幕监听终极指南&#xff1a;基于系统代理技术的直播数据抓取实战教程 【免费下载链接】DouyinBarrageGrab 基于系统代理的抖音弹幕wss抓取程序&#xff0c;能够获取所有数据来源&#xff0c;包括chrome&#xff0c;抖音直播伴侣等&#xff0c;可进行进程过滤 项目地址:…

作者头像 李华