DeepSeeker-Code源码导读06-工具协议CustomTool

读懂 CustomTool:一个工具协议怎么把约束和优化结构化

这篇讲什么

上一篇讲"信号归模型、约束归 harness"。那约束的具体载体是什么?答案就在这篇要拆的 CustomTool 协议里。

OpenAI 的工具调用标准(ChatCompletionTool)很朴素------只有 function: { name, description, parameters },够模型知道"有这么个工具、怎么调"。但一个工业级 agent 的工具,要回答的问题远不止这些:这个工具危不危险?要不要审批?结果太长怎么裁?跑完了模型会不会无视报错?这些全是 OpenAI 标准管不着的。

DeepSeeker-Code 的 CustomTool 在标准之上扩了五大层字段,把安全、并发、上下文、防幻觉 全套工程能力结构化进了协议。type.ts 这一个文件,就是整个工具系统的"宪法"。这篇拆它。

一、先看全貌:五大层字段

读这篇的钥匙,是下面这张字段全景表。它按"一个工具从定义到结果回灌"的生命周期,列出五大层各管什么、谁消费、何时生效:

代表字段 解决什么问题 谁消费 何时生效
执行调度 execute / isSync 怎么执行、阻塞还是后台 执行层 执行时
安全风控 safetyLevel / requireApproval / privacyMaskingRules 危不危险、要不要审、脱不脱敏 guard 审批网关 执行前
并发锁 exclusiveLock 防多工具并发改脏 workspace 执行层调度 执行前/中
上下文优化 maxOutputCharacters / outputFilter 结果太长怎么裁、给谁看 执行层 结果回灌时
环境防幻觉 validateEnvironment / verifyResult / timeoutMs 没条件就别暴露、跑完别让模型瞎乐观 喂模型前 / 结果回灌 喂模型前 / 执行后

注意最后一列------这些字段的生效时机横跨"喂模型之前"到"结果回灌之后"。也就是说,一个工具的字段,参与了它从"暴露给模型"到"结果喂回模型"的全过程,不只是执行那一刻。

拿几个场景盘一下,你会看到这些字段实际怎么起作用:

  • 场景 A:模型调 read_file 读 .env。 safetyLevel=SAFE(读操作免审),但 privacyMaskingRules 生效------执行层在把文件内容发给云端 DeepSeek 之前,把密钥替换成 [MASKED_SECRET]。本地读得到,云端看不到。

  • 场景 B:模型调 edit_file 改文件。 safetyLevel=MUTATIONrequireApproval 回调根据参数生成精准提示(具体改哪个文件、改什么)。guard 网关弹窗,用户点"通过"才执行。

  • 场景 C:模型调 run_command 跑 npm install,吐了 2000 行进度条。 outputFilter 把结果分成两路:toUser(2000 行进度条,给用户看动画)和 toModel(一句"Successfully installed 45 packages",喂给模型)。maxOutputCharacters 再兜底截断。

  • 场景 D:模型调 run_command 跑 tsc,编译报错了,但模型想说"成功了"。 verifyResult 硬编码分析 stderr,判定 FAILED,执行层在结果置顶插一行警告"系统判定失败,别盲目乐观"。

  • 场景 E:本机没装 chrome。 validateEnvironment 返回 false,runAgent 根本不把 view_webpage 暴露给模型------模型连"有这个工具"都不知道,不会无效调用。

理解了这些字段各自解决什么,再去看协议定义,那些字段就不是孤立的配置项,而是一套环环相扣的工程护城河。下面逐层拆。

二、逐层拆解

执行调度层:execute + isSync

最基础的两件事------怎么执行、阻不阻塞。

ts 复制代码
execute: (args, ctx) => Promise<string> | AsyncGenerator<string>;
isSync: boolean;

execute 支持两种返回:Promise<string>(普通)和 AsyncGenerator<string>(流式)。流式是为了实时输出 ------比如 tail -f 式的日志、长任务的进度,边产出边吐给前端,不用等全部跑完。

isSync 决定阻不阻塞。true 是同步(执行层等它返回才继续);false 允许后台挂起------比如启动一个 dev server,它不该阻塞 agent,而是挂后台持续跑。这个字段和下一层的 exclusiveLock 配合,是"后台任务管理"的基础。

安全风控层:safetyLevel + requireApproval + privacyMaskingRules

这是字段最多、也最重要的一层。三个字段各管一件事:

ts 复制代码
safetyLevel: ToolSafetyLevel;  // SAFE / MUTATION / DANGER 三级
requireApproval?: string | ((args, ctx) => string | Promise<string>);
privacyMaskingRules?: RegExp[] | ((args, rawOutput) => string);

safetyLevel 是初筛 ------三级自动化的平衡点。SAFE(读操作,完全免审,保流畅)、MUTATION(写操作,配置 --yes 或免审目录时可自动放行)、DANGER(高危,强制弹窗)。这个分级是"极致自动化 vs 绝对安全"的取舍:读类一律放行保流畅,写类看配置,高危一律拦。

requireApproval 是声明式风险标记 ------但它有个关键设计(注释 :127):

真正的拦截由执行层统一完成,工具本身不感知弹窗协议。

这正是上一篇"约束归 harness"的延续:工具只"声明"自己需要审批(requireApproval 产出一段说明),但"弹窗、等待、放行/拒绝"这套协议由执行层(guard 网关)统一处理。 工具不关心是 CLI 弹窗还是 VSCode 弹窗,它只管"我要审批,这是说明"。而且它支持动态回调------结合参数生成精准提示(显示具体的 git diff、即将被删的路径),而不是千篇一律的"确认执行?"。

privacyMaskingRules 是数据防外泄 。注释点明了场景(:134):

DeepSeek 是云端模型,当本工具(如 read_file)读到 .env 或含有密码的文件时,执行层在把文本发给云端前,会根据此策略将密钥替换为 [MASKED_SECRET]

这是"单人本地工具 + 云端模型"定位下的硬约束------模型在你的机器上读文件,但它本身在云端,读到的内容要走网络。敏感数据必须在"发给云端前"就地脱敏。这个字段把"脱敏"也从工具里抽出来、变成声明式策略。

并发锁层:exclusiveLock

ts 复制代码
exclusiveLock?: string | ((args, ctx) => string);

一个工具要独占某个资源时,返回一个锁标识。注释说得很清楚(:143):

当一个 isSync: false 的后台任务占有该锁时,后续相同锁特征的工具调用将被挂起或拒绝。

场景:你启动了一个后台 dev server(isSync:false,占着 bash_session_lock),这时如果又来个工具要改同一个 workspace 的状态,就会冲突。锁让后到的调用挂起或拒绝,防多线程脏写。锁标识可以是具体的敏感文件路径,也可以是 "bash_session_lock" 这种逻辑锁。

上下文优化层:maxOutputCharacters + outputFilter

这层管"工具产出的东西怎么喂回去",是大模型注意力经济的体现(设计理念"设局七条"之一)。

ts 复制代码
maxOutputCharacters?: number;
outputFilter?: (rawOutput) => { toModel: string; toUser: string };

maxOutputCharacters 是裁剪阈值,超过就用第 4 篇讲的 truncateToolResult 去中间留头尾------防巨量日志/大文件撑爆上下文。

outputFilter 是最精巧的一个------双通道分离 。注释的例子(:158)非常形象:

执行 npm install 产生了 2000 行依赖下载进度条。

  • toUser: 终端用户需要实时看到的酷炫安装动画。
  • toModel: 真正喂给大模型上下文的纯净结论("Successfully installed 45 packages.")。

用户和模型,想看的东西不一样。用户要"过程感"(进度条、动画),模型要"结论"(装了几个包)。如果把这 2000 行全喂给模型,既烧 token,又稀释模型对真正重要信息的注意力。outputFilter 把一次工具产出劈成两路:给用户看的走前端,给模型看的走上下文。

这是个深刻的设计------"用户注意力"和"模型注意力"是两种不同的资源,要分开管理。 很多人让 agent 把工具的完整输出原样塞回上下文,结果模型被无关细节淹没。outputFilter 从协议层面杜绝了这个问题。

环境与防幻觉层:validateEnvironment + verifyResult + timeoutMs

这层最有意思,三个字段各有故事。

validateEnvironment 是前置剔除:171):

ts 复制代码
validateEnvironment?: (ctx) => boolean | Promise<boolean>;

把工具喂给模型之前 ,先本地跑这个断言。返回 false,runAgent 直接从工具表里删掉这个工具。场景:view_webpage 需要本机有 chrome,没有就根本不暴露给模型------模型连"有这个工具"都不知道,不会无效调用。这是"不暴露做不到的能力",比"暴露了再报错"优雅得多。

verifyResult 是防幻觉的硬编码断言:187):

ts 复制代码
verifyResult?: (rawOutput, ctx) => ({
    status: ToolExecutionResultStatus,  // SUCCESS/FAILED/TIMEOUT/ABORTED
    errorCategory?: 'syntax' | 'runtime' | 'permission' | 'unknown',
    summary?: string,
});

这是治模型一个典型毛病------选择性无视报错 。模型跑完 tsc,stderr 里明明一堆编译错误,它却能乐观地说"编译成功"。verifyResult 让工具底层自己分析 stdout/stderr ,硬编码判定状态。执行层收到 FAILED,就在给模型的 ToolResult 置顶插一行(:184):

【系统判定】:该工具执行结果为失败,请停止乐观盲目幻想,立刻仔细阅读下方报错并修正参数!

这是"确定性"压过"模型自觉"------不指望模型自己判断成不成功,用代码硬判定。 模型可以无视软提示,但很难无视一句置顶的"系统判定失败"。

timeoutMs 是刻意不消费的字段:174)------这个最值得讲。它定义了,但附长注释明确"别接入执行层":

对标 Claude Code:CC 不在工具级挂固定定时器(会误杀合法的长构建/测试/install),而是用「每调用由模型可控的 timeout 参数」+ 后台模式。本框架沿用同一原则:取消统一由用户主动中断(ctx.abortSignal)驱动,长任务走 isSync:false 后台。故本字段保留定义但不接入执行层------别被"预留"字眼诱惑而重接(会引入误杀)。

定义了一个字段却刻意不用,还特意写注释警告后人别接------这是克制的智慧 。固定超时听起来安全,实际会误杀合法的长任务(一次正经的构建可能要几分钟)。所以取消交给用户(abortSignal),长任务走后台,不在工具级挂死定时器。知道一个能力存在,但判断它弊大于利而不用,比盲目堆功能难得。

三、四个关键设计决策

决策一:工具不感知审批协议

requireApproval 只声明"我要审批 + 说明",真正的弹窗/等待/放行由 guard 执行层统一做。好处是工具和宿主解耦------同一个工具,在 CLI 走终端模态审批、在 VSCode 走 webview 审批、在 HTTP 走 SSE 审批,工具代码一行不用改。这是上一篇"约束归 harness"在工具协议层的落地。

决策二:toModel/toUser 双通道,注意力分离

outputFilter 把一次产出劈成"给用户的"和"给模型的"两路。背后是"大模型注意力经济"------上下文里每一行无关内容,都在稀释模型对真正重要信息的关注。用户要看过程,模型要看结论,两者必须分流。 这个决策让工具产出天然适合 agent 场景,而不是把终端输出原样塞上下文。

决策三:verifyResult 用确定性压过模型自觉

不指望模型自己判断工具成败,用工具底层的硬编码分析来判定。这是对模型"乐观幻觉"的直接对冲。和第 1 篇的 nudge(防空手收尾)、repeatBreaker(防死循环)一脉相承------凡模型容易自欺的地方,都用代码硬兜。

决策四:timeoutMs 刻意不消费

定义了能力但判断弊大于利而不用,对标 Claude Code。固定超时会误杀合法长任务,所以取消交给用户、长任务走后台。这个决策体现的是"克制比堆功能难"------预留一个字段、写注释警告别接,是为了抵抗"它都定义了不如接上"的诱惑

四、五个技术难点

难点一:SAFE/MUTATION/DANGER 三级怎么划

三级是"自动化流畅度 vs 安全"的平衡点。划得太细(五级六级),模型和用户都记不住;划得太粗(只分读写),又区分不出"改文件"和"跑 rm -rf"这种天壤之别。最终的三级是经验解:读一律 SAFE 保流畅,写 MUTATION 看配置可放行,高危 DANGER 一律拦。这个划分不是理论推导,是反复拿捏"什么能自动、什么必须人审"的结果。

难点二:requireApproval 的动态回调

固定字符串提示("确认执行 edit_file?")信息量太低。动态回调让提示带参数细节------具体改哪个文件、diff 是什么、要删哪个路径。难点在于"生成精准提示"本身可能很贵(算 diff 要读文件),所以它支持 Promise<string>,允许异步生成。这让审批弹窗从"千篇一律的确认"变成"带上下文的有意义决策"。

难点三:outputFilter 的双通道实现

把一次原始输出劈成两路,难点是"什么该给模型、什么该给用户"没有统一标准------每个工具的"信号 vs 噪声"不一样。npm install 是"进度条(噪声)vs 安装结论(信号)",但别的工具可能反过来。所以 outputFilter 是每工具定制的,不是通用规则。这个设计承认了"注意力分离没有银弹",把决定权下放给每个工具。

难点四:verifyResult 防幻觉的判定依据

怎么"硬编码判定成败"?靠分析 stdout/stderr 的特征------编译器报错的关键词、退出码、特定模式。难点是不同工具的失败特征不同(tsc 的报错和 npm 的报错长得不一样),所以 verifyResult 也是每工具定制。它不追求 100% 准确,追求"比模型自觉可靠"------模型会乐观,代码不会。

难点五:validateEnvironment 的前置剔除时机

难点不在断言本身,而在何时跑。它必须在"工具表喂给模型之前"跑(systemInjections.ts 的 filterByEnvironment),否则模型已经看到工具了、还可能去调。这个时机卡得很死------晚一步,剔除就没意义了。所以它和工具表裁剪(计划模式、环境过滤)在同一阶段完成。

五、推荐的源码阅读顺序

  1. 先通读 type.ts 全文。它不长,但密度极高。按五大层的注释分区(3.1~3.5)读,先建立"一个工具协议有哪些字段"的全景。
  2. 重点读每个字段的注释。这个文件的注释质量极高,每个字段都写了"场景"和"谁消费"------注释本身就是文档。
  3. 挑一两个真实工具看字段怎么赋值 。比如 registry/fs.ts(read_file/edit_file,看 safetyLevel/requireApproval/privacyMaskingRules)、registry/command.ts(run_command,看 verifyResult/outputFilter/isSync)。把抽象字段对应到具体用法。
  4. 读 systemInjections.ts 的 filterByEnvironment:看 validateEnvironment 在哪个阶段剔除工具。
  5. 读 guard.ts(下一篇):看 requireApproval/safetyLevel 怎么被审批网关消费------这篇讲"声明",下一篇讲"执行"。

六、关联:协议是约束的载体

CustomTool 这套字段,是前几篇诸多设计的落脚点

  • safetyLevel / requireApproval → 下一篇(第 7 篇)的五道安全防线,审批网关就是消费这两个字段的执行层。
  • outputFilter / maxOutputCharacters → 第 4 篇的上下文压缩,工具产出裁剪是压缩的"输入端治理"。
  • validateEnvironment → 第 10 篇的 tsHost,TypeScript 工具的环境断言(typescript 是否可用)就用这个字段。
  • exclusiveLock / isSync → 后台任务管理、worktree 隔离(第 5 篇的 run_workflow worktree)的基础。
  • verifyResult → 第 1 篇的防幻觉体系(nudge/repeatBreaker)在工具层的延续。

你会看到,"约束归 harness"不是一句空话------它具体落到 CustomTool 的这些字段上。每个字段,都是把一类"该由 harness 而非模型负责的事",结构化进了协议。读工具系统,先读懂这个协议,后面所有工具的实现就都有了框架。

最后

OpenAI 给了工具调用一个最简协议,DeepSeeker-Code 在上面扩了五大层。这不是过度设计------每一层都对应一个工业级 agent 绕不开的问题:安全(别乱来)、并发(别脏写)、上下文(别撑爆)、防幻觉(别瞎乐观)、环境(别给做不到的)。

读这个协议,最值得带走的不是具体字段,而是**"把约束和优化结构化进协议"这个思路**。一个工具不只是一段 execute 逻辑,它还是一组关于"怎么被安全、高效、可靠地使用"的声明。这些声明让工具系统能在"模型自主"和"工程可控"之间找到平衡------而这,正是整个 agent 可用性的根基。

七、补记:edit_file 的批量原子编辑

这篇成文后,edit_file 又落了一个值得记录的设计------同文件多处修改的批量原子模式。它是"减少 agent 轮次"这条主线在工具层的落点。

问题长这样:模型要改一个文件的 5 处地方。旧协议一次调用只收一对 old_str/new_str,于是模型连调 5 次 edit_file------而每调一次,是一轮完整的"推理 → 工具调用 → 结果回灌"。5 处修改就是 5 轮 LLM 往返,延迟和 token 双倍涨,可这 5 处修改在模型脑子里本来就是一个整体。

为什么不能让模型"合并成一次大编辑" ?把首尾两处修改之间的所有未改动代码都包进 old_str------那要逐字符复刻中间几百行,任何一处空白对不上就整体失败,匹配成功率随长度骤降。整文件重写?大文件会截断/幻觉,还丢掉了"锚点定位"这个安全边界。而且串行有时序硬约束:第 1 处改完后文件变了,第 2 处的 old_str 可能要锚在前面修改引入的新代码上------多处修改天然是"按序应用",不是无序集合。

解法是让协议收数组,而不是让模型把活干得更粗fs.ts):

  • 新增 edits: [{old_str, new_str, replace_all?}, ...],按数组顺序依次应用;第 N 条的 old_str 基于前 N-1 条应用后的内容构造------这条规则直接写进 schema description 教给模型;
  • 整体原子:任一条未命中,全部不写入、文件零改动。错误信息精确定位到"第 2/5 条未命中",模型只需修正那一条、整组重试;全部成功也只写一次盘(TOCTOU 围栏复检、Undo 备份随之收口到一处);
  • 单条老参数(顶层 old_str/new_str)完全兼容------等价于长度 1 的批量。

实现上,就是把原 execute 里的匹配逻辑抽成纯函数 applyOneEditToContent(多级容错匹配原样保留),execute 变成"读一次文件 → 循环应用 → 全成才写一次"。5 处修改从 5 轮往返降到 1 轮,审批弹窗也从 5 个合成 1 个带完整 diff 的。

同一批改动里,search_grep 加了 context 参数与文件定向、get_diagnostics 加了 paths 批量、glob 支持了花括号多选(*.{ts,vue} 一次查两类)------全是同一条原则的推论:当"模型多次调用"的根因是协议一次只收一个单元时,正确方向是让协议收数组,而不是指望模型自己省着调用。

下一篇,我们顺着 safetyLevelrequireApproval,读审批网关 processToolCall------看这些声明怎么被执行层消费,变成五道实打实的安全防线。

项目源码开源在 github.com/xnk/deepSee... ,文章里提到的文件都在 src/core/src/tool/ 下,欢迎对着源码读。觉得这个导读系列有点意思,点个 star 是对我最大的鼓励。

总结

  1. CustomTool = OpenAI 标准 + 五大层扩展:执行调度(execute/isSync)、安全风控(safetyLevel/requireApproval/privacyMaskingRules)、并发锁(exclusiveLock)、上下文优化(maxOutputCharacters/outputFilter)、环境防幻觉(validateEnvironment/verifyResult/timeoutMs);
  2. 字段参与工具全生命周期:从"喂模型前剔除"到"结果回灌裁剪",不只是执行那一刻;
  3. 四个设计决策:工具不感知审批协议(声明归工具、拦截归 harness)、toModel/toUser 双通道(用户注意力 vs 模型注意力分离)、verifyResult 用确定性压过模型自觉、timeoutMs 刻意不消费(对标 CC 的克制);
  4. 五个技术难点:三级安全怎么划(自动化平衡点)、requireApproval 动态回调(精准提示)、outputFilter 双通道(每工具定制的注意力分离)、verifyResult 判定依据(硬编码比模型自觉可靠)、validateEnvironment 时机(喂模型前剔除);
  5. 协议是约束的载体:前几篇"约束归 harness"的设计,具体落地到这些字段上。
相关推荐
Patrick_Wilson1 小时前
当执行不再稀缺:AI Agent 时代的技术判断力
人工智能·架构·ai编程
Vuji1 小时前
Pi 插件解剖|ssh.ts:只用 221 行,让 Agent 直接在远程机器干活
前端·人工智能·agent
dong_junshuai1 小时前
每天一个开源项目#73 Munder Difflin:2.3K Star 的本地多Agent办公室
开源·github·agent
SpaceAIGlobal1 小时前
AI PPT生成工具哪些支持PDF文档导入?
人工智能·ai·pdf·powerpoint·办公
leeyi1 小时前
Langfuse 集成源码:batch 协议、media 上传与 mock 测试(第89篇-E75)
llm·aigc·agent
王中阳Go1 小时前
杰富瑞实测 8 款 AI Agent,国产千问 95 分登顶:我连夜把项目的 OpenAI 硬编码全拆了
人工智能·go
hyunbar7771 小时前
Tools、Function Calling、MCP 都在说什么?
人工智能
智能运维指南2 小时前
智能体自治运维选型指南:2026年企业如何从“AI辅助”走向“AI自治”
大数据·运维·人工智能
修远客2 小时前
风格进化:让Agent越来越懂你 — 从"工具"到"助手"的关键跃迁
llm·agent
桃西西呀2 小时前
dsh能接生产吗?fail-closed 沙箱到底保不保底
人工智能