跳到主要内容
《Agent Runtime 工程化》第四章 工具系统设计 | 极客日志
编程语言 Agent Runtime 工程化 陈堂会
《Agent Runtime 工程化》第四章 工具系统设计 这一章开始给工具“上户口”。在 demo 里,工具是几个函数;在 Agent Runtime 里,工具是一套能力治理系统。 模型负责提出“我想调用哪个工具、传什么参数”。Runtime 负责判断这件事是否存在、参数是否合法、风险是否可接受、是否需要用户确认、怎样执行、输出怎样进入上下文、失败怎样反馈。把这些责任都塞进一个 execute() 函数,是很多 A
站点编辑 发布于 2026/8/15 更新于 2026/8/16 3 浏览
书名 :《Agent Runtime 工程化:从工具调用循环到可恢复执行系统》
作者 :陈堂会
平台 :极客日志(https://zeeklog.com)
X :@Megick_com
联系邮箱 :[email protected]
章节 :第四章 工具系统设计
第四章 工具系统设计
这一章开始给工具'上户口'。在 demo 里,工具是几个函数;在 Agent Runtime 里,工具是一套能力治理系统。
模型负责提出'我想调用哪个工具、传什么参数'。Runtime 负责判断这件事是否存在、参数是否合法、风险是否可接受、是否需要用户确认、怎样执行、输出怎样进入上下文、失败怎样反馈。把这些责任都塞进一个 execute() 函数,是很多 Agent 项目从 demo 走向真实使用时最先踩的坑。
工具系统的目标不是让模型拥有更多按钮,而是让 runtime 能够稳定、安全、可追溯地使用这些按钮。
图 4-1:工具治理管线。一个工具从函数进入 runtime,要经过定义、schema 校验、风险分类、权限预览、执行、结果归一化和上下文预算。
4.1 Tool Registry 不只是 Map
最常见的入门写法是:
const tools = new Map <string , Function >();
这个写法能跑,但很快会失控。因为真实工具不只有名字和函数。它还要告诉 runtime:自己会不会写文件,会不会联网,会访问哪些资源,默认超时多久,输出太大怎么办,失败是否可以重试,用户看到审批弹窗时应该看到什么。
一个可用的工具定义至少应包含这些字段:
type RiskLevel = "read" | "write" | "network" | "install" | "destructive" ;
type <I = , O = > = {
: ;
: ;
: ;
: ;
: ;
: ;
: [];
: {
: | | | | ;
: ;
}[];
: ;
: {
: ;
: ;
: ;
};
?: < >;
( : I, : ): <O>;
};
ToolDefinition
unknown
unknown
name
string
title
string
description
string
version
string
inputSchema
unknown
riskLevel
RiskLevel
sideEffects
string
resources
kind
"filesystem"
"shell"
"network"
"mcp"
"database"
scope
string
timeoutMs
number
outputPolicy
maxCharsForModel
number
summarizeWhenLarge
boolean
redactSecrets
boolean
preview
(input : I, ctx : ToolContext ) =>
Promise
ToolPreview
execute
input
ctx
ToolContext
Promise
消费者 读取的字段 用途 模型 name、description、inputSchema判断是否调用、如何填参数 Permission Gate riskLevel、sideEffects、resources、preview自动允许、询问用户或拒绝 Executor timeoutMs、资源限制、取消信号控制执行过程和失败语义 Context Builder outputPolicy、结果类型、截断标记决定注入全文、摘要或引用
少填一个字段,系统不会立刻报错,但后面某个模块会用猜的方式补上。工程事故经常就是从'先随便填一下'开始的。
4.2 工具描述要写给模型,也要写给人 description 会影响模型选择工具。写得太泛,模型会乱用;写得太短,模型不知道何时该用;写得像广告,模型会误判能力边界。
Run a read-only shell command in the current workspace. Use this for inspecting files,
searching text, counting lines, or printing small file snippets. Do not use it for
writing files, installing dependencies, network access, or destructive operations.
这段描述把用途和禁区都写出来了。模型看到的工具说明越具体,runtime 后面拦截的次数越少。
不过,描述永远不能替代策略。即使描述写了'只读',runtime 仍要检查命令内容。模型可能误填,MCP Server 也可能把工具描述写得含糊甚至不可信。工具描述是提示,工具策略才是边界。
4.3 Schema 表达形状,Policy 表达行为 TypeScript 生态里,Zod 常用于运行时校验;模型工具调用和 MCP 常围绕 JSON Schema 描述输入。工程上可以用 Zod 编写工具输入,再转换成 JSON Schema 暴露给模型或协议层。
import { z } from "zod" ;
const SearchCodeInput = z.object ({
query : z.string ().min (1 ).describe ("ripgrep-compatible search text" ),
path : z.string ().default ("." ),
maxResults : z.number ().int ().min (1 ).max (100 ).default (50 ),
});
type SearchCodeInput = z.infer <typeof SearchCodeInput >;
const searchCodeTool : ToolDefinition <SearchCodeInput , SearchResult > = {
name : "search_code" ,
title : "Search code" ,
description : "Search text in the workspace using ripgrep. Read-only." ,
version : "1.0.0" ,
inputSchema : SearchCodeInput ,
riskLevel : "read" ,
sideEffects : [],
resources : [{ kind : "filesystem" , scope : "workspace" }],
timeoutMs : 15_000 ,
outputPolicy : {
maxCharsForModel : 12_000 ,
summarizeWhenLarge : true ,
redactSecrets : true ,
},
async execute (input, ctx ) {
return ctx.search .run (input);
},
};
第一,schema 要表达约束,而不是只写类型。path 不应只是任意字符串,command 不应只是任意字符串,url 不应只是任意字符串。越靠近文件系统、shell、网络、数据库,schema 越要具体。
第二,schema 不能替代 policy。{ "command": "rm -rf ." } 在形状上是合法字符串,但行为上危险。参数合法只说明'长得对',不说明'能执行'。
层级 回答的问题 示例 Schema validation 参数形状对不对 maxResults 是否是 1 到 100 的整数Policy validation 行为是否允许 命令是否只读,路径是否在 workspace 内 Approval decision 是否需要人确认 写文件、安装依赖、联网请求是否展示 preview
4.4 工具版本会影响 Agent 行为 工具一旦进入 trace 和 eval,就不能随意改语义。比如 read_file 原来默认返回全文,后来默认只返回前 200 行;这会改变模型后续行为,也会改变 replay 结果。
type ToolVersion = {
name : string ;
version : string ;
changes : string [];
compatibleWith ?: string [];
};
增加可选字段通常是兼容变更;
删除字段、改变默认值、改变输出结构,应视为破坏性变更;
trace 里记录工具版本;
eval case 里记录工具版本;
replay 旧 trace 时,要么使用旧 adapter,要么显式迁移。
很多团队只版本化 prompt,却忘了工具也在决定行为。一个 Agent 为什么突然变笨,可能不是模型换了,而是 search_code 的默认 maxResults 从 100 变成了 10。
4.5 执行前先生成计划 工具执行前,runtime 应把模型返回的 tool calls 转成自己的 ToolCallPlan。这一步会完成工具查找、schema 校验、风险分类、资源声明和审批策略判断。
type ToolCallPlan = {
call : ToolCall ;
tool : ToolDefinition ;
parsedInput : unknown ;
riskLevel : RiskLevel ;
resources : string [];
approval : "auto" | "ask" | "deny" ;
preview ?: ToolPreview ;
};
async function planToolCall (call : ToolCall , registry : ToolRegistry , ctx : ToolContext ) {
const tool = registry.get (call.name );
if (!tool) return unknownToolPlan (call, registry.names ());
const parsed = parseWithSchema (tool.inputSchema , call.input );
if (!parsed.ok ) return invalidArgsPlan (call, parsed.error );
const policy = await ctx.policy .evaluate (tool, parsed.value );
const preview = tool.preview ? await tool.preview (parsed.value , ctx) : undefined ;
return {
call,
tool,
parsedInput : parsed.value ,
riskLevel : tool.riskLevel ,
resources : policy.resources ,
approval : policy.decision ,
preview,
} satisfies ToolCallPlan ;
}
计划阶段的好处是:你可以在真正执行前把风险摊开。CLI 可以打印命令预览,IDE 可以展示 diff,非交互模式可以根据 policy 自动拒绝。更重要的是,trace 可以记录'为什么这个工具被允许或拒绝',而不是只记录执行结果。
4.6 并行工具调用由 runtime 调度 模型可以一次返回多个 tool calls,但能不能并行执行,必须由 runtime 决定。并行能提升效率,也会放大风险。
只读工具可以考虑并行;
写入工具默认串行;
访问同一资源的工具默认串行;
高风险工具必须等待审批结果;
所有并行结果进入上下文前要统一做预算裁剪。
function canRunInParallel (a : ToolCallPlan , b : ToolCallPlan ): boolean {
if (a.approval !== "auto" || b.approval !== "auto" ) return false ;
if (a.riskLevel !== "read" || b.riskLevel !== "read" ) return false ;
if (intersects (a.resources , b.resources )) return false ;
return true ;
}
不要让模型直接决定并行。模型提出意图,runtime 安排执行。这个边界必须守住。
实战里还有一个细节:并行工具的输出顺序要稳定。即使执行完成顺序不同,写回消息历史时也应按模型 tool call 顺序或明确的调度顺序排列。否则 replay 和 eval 会变得很难解释。
4.7 权限预览决定用户是否敢信任 Agent 高风险工具执行前,应给用户看到 preview。用户不是审批机器,他需要知道 Agent 为什么要做这件事,会影响什么。
写文件要给 diff preview;执行命令要给 command preview;联网请求要给域名、方法、数据范围;安装依赖要给包名、版本、registry、lockfile 影响;数据库操作要给目标库、表、迁移摘要。
图 4-2:工具系统的风险矩阵。风险来自能力、参数和上下文三者的组合,而不只是工具名称。
type ApprovalRequest = {
callId : string ;
toolName : string ;
riskLevel : RiskLevel ;
preview : {
title : string ;
body : string ;
diff ?: string ;
command ?: string ;
affectedResources : string [];
};
reason : string ;
decisionMode : "auto" | "ask" | "deny" ;
};
reason 很重要。不要只弹出 Allow run_command?。好的审批请求会说清:Agent 为什么需要这个命令,命令是否会写文件,预期输出是什么,不执行会怎样降级。
非交互模式更要明确策略。CI 或后台任务不能弹窗,所以必须有 policy 文件:
{
"tools" : {
"read_file" : "auto" ,
"search_code" : "auto" ,
"edit_file" : "deny" ,
"run_command" : {
"safe_read" : "auto" ,
"write" : "deny" ,
"network" : "deny" ,
"destructive" : "deny"
}
}
}
交互模式和非交互模式使用同一套风险分类,只是决策方式不同。这样系统行为才一致。
4.8 失败分类要成为协议 工具失败不应只有 error: string。失败分类是模型、runtime、trace 和 eval 之间的协议。
类型 例子 Runtime 行为 INVALID_ARGUMENTS参数缺字段、类型错 反馈模型重新调用 UNKNOWN_TOOL模型调用未注册工具 提供简短可用工具列表 NOT_FOUND文件不存在 建议搜索或列目录 PERMISSION_DENIED用户拒绝或策略拒绝 停止或要求模型找低风险方案 TIMEOUT命令超时 截断输出,允许有限重试 TOOL_CRASH工具内部异常 记录 trace,通常停止或降级 OUTPUT_TOO_LARGE日志过长 摘要、截断或保存 artifact SIDE_EFFECT_UNCERTAIN写入中断 进入 checkpoint 检查和恢复流程
失败分类不是为了把错误消息写得更漂亮。它让模型知道下一步该怎么做,也让 eval 能统计'改动后是否减少了非法参数错误'。如果所有失败都叫 FAILED,后面就只能靠人工读日志。
建议每个工具至少测试三类失败:参数失败、执行失败、输出过大。只测 happy path 的工具,一进入 Agent Loop 就会把复杂性转嫁给模型。
4.9 输出策略是上下文工程的入口 工具输出不是越完整越好。rg 搜索可能返回几百个匹配;测试命令可能输出几万行日志;读取大文件可能直接挤掉用户目标。工具定义里必须写清输出策略。
type ToolOutputPolicy = {
maxCharsForModel : number ;
summarizeWhenLarge : boolean ;
artifactWhenLarge : boolean ;
redactSecrets : boolean ;
priority : "low" | "normal" | "high" ;
};
输出进入模型前,建议统一走 normalizeToolResult():
async function normalizeToolResult (raw : unknown , policy : ToolOutputPolicy ) {
const text = stringifyToolOutput (raw);
const redacted = policy.redactSecrets ? redactSecrets (text) : text;
if (redacted.length <= policy.maxCharsForModel ) {
return { content : redacted, truncated : false };
}
if (policy.artifactWhenLarge ) {
const artifactRef = await saveArtifact (redacted);
return {
content : `工具输出过长,已保存为 artifact:${artifactRef} 。以下是摘要:\n${summarize(redacted)} ` ,
truncated : true ,
};
}
return {
content : redacted.slice (0 , policy.maxCharsForModel ),
truncated : true ,
};
}
这段逻辑会在第五章继续展开。这里先把入口放在工具系统里:每个工具都要声明自己的输出价值和风险。
4.10 本章的实现路径 把第三章的 mini-agent 扩展为工具治理系统时,建议按这个顺序做。
第一步,把 ToolDefinition 从函数表升级为带 schema、风险等级、资源声明、超时和输出策略的结构。
第二步,为 list_files、read_file、shell_readonly 补齐风险字段和 policy 校验,确认旧功能没有退化。
第三步,新增 search_code。它通常是 coding agent 中最高频的只读工具,要重点处理结果数量、路径范围和截断策略。
第四步,新增 edit_file 和 apply_patch,但先只生成 preview,不真正写入。等 preview 稳定后,再接审批和执行。
第五步,新增 run_command,把命令分类为 read、write、network、install、destructive。不要追求第一次就识别所有命令,先把默认策略设为保守。
4.11 验收标准 完成本章后,你应该能说清楚:工具系统为什么是一套治理层,而不是一串函数;schema 校验、policy 校验和用户审批有什么区别;并行工具调用如何与审批交织;工具失败如何变成模型可理解的 observation。
用例 期望结果 search_code 搜索普通关键词自动执行,结果按数量和长度限制返回 read_file 读取 workspace 外路径policy 拒绝,返回 PERMISSION_DENIED edit_file 修改文件先生成 diff preview,等待确认 run_command 执行 npm install分类为 install,展示包管理器和 lockfile 风险 run_command 执行 rm -rf .默认拒绝或强确认,写入 audit log 工具输出超过预算 返回摘要或 artifact 引用,并标记 truncated
做到这一步,你的 Agent 已经不只是'会调用工具',而是开始具备工程边界。它知道能力从哪里来,也知道能力不能越过哪里。
系列文章目录 相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online