深色模式
Demo 6 · 注册工具并观察工具循环
目标:在真实 headless 链路上注册一个
echo工具,让 Mock 适配器发起工具调用,观察"一个 turn 两个 step"的完整工具循环与日志落盘。对应原理篇 06。
运行
sh
cd demos/06-tool-echo
DSH_HOME="$PWD/.dsh-home" npx dsh --profile headless --patch tool.patch.yml "请 echo 一句话验证工具链路"预期输出:
text
[echo-tool] 已注册 echo 工具
[echo-tool] 收到调用:args={"text":"来自 MockAdapter 的问候"} callId=call-echo-1
echo 工具返回了:「来自 MockAdapter 的问候」。任务完成。三行输出对应工具循环的三个阶段:注册 → 执行 → 结果回喂模型。
代码拆解
echo 工具插件(plugins/echo-tool.ts)
ts
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'echo-tool'
export const inject = ['tools'] as string[] // 等工具注册表就绪
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'echo',
description: '把传入的文本原样返回。用于验证工具调用链路。',
parameters: {
text: { type: 'string', required: true, description: '要原样返回的文本' },
repeat: { type: 'number', description: '重复次数,默认 1' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value as string }],
},
async execute(args, exec) { // args 类型由 parameters 推导
const n = Math.max(1, Math.min(args.repeat ?? 1, 10))
return args.text.repeat(n)
},
}))
}三个面各就各位(原理篇 6.1):模型面(name/description/parameters → JSON Schema 进入提示词装配)、执行面(execute 拿到校验后的 args 与受保护的 exec)、渲染面(output.render 产出持久化内容)。
Mock 适配器的"工具决策"(plugins/mock-adapter.ts)
ts
const shouldCallEcho = Boolean(echo) && !hasToolResult(options.messages) && userText.includes('echo')
// 第一步:请求带 echo 工具、任务含 "echo"、消息里还没有工具结果 → 发起工具调用
// 第二步:消息里有工具结果 → 基于结果作答这一步演示的是适配器侧看到的真实协议:agent-loop 第二次调用模型时,消息里会带上 tool-result 块。真实模型也是这样工作的——看到工具结果,然后继续作答。
输出解读:一个 turn,两个 step
用 node read-session.mjs 看日志,节选:
text
4 turn/start {"turn":1}
6 step/start {"turn":1,"step":1}
7 user/message 「请 echo 一句话验证工具链路」
27 assistant/message 「我来调用 echo 工具验证一下。」
28 tool/call echo({"text":"来自 MockAdapter 的问候"})
30 tool/result [{"type":"tool-result","toolCallId":"call-echo-1",…}]
31 step/end {"turn":1,"step":1}
32 step/start {"turn":1,"step":2}
50 assistant/message 「echo 工具返回了:「来自 MockAdapter 的问候」。任务完成。」
51 step/end {"turn":1,"step":2}
52 turn/end {"kind":"completed"}对照原理篇 5.2 的时序图:
- step 1 = 第一次模型请求(seq 27 的 assistant/message)+ 它发起的工具调用(seq 28/30)——工具循环闭合后
step/end; - step 2 = 第二次模型请求(seq 50,消息里带着工具结果)——模型不再要求工具,turn 结束。
"工具循环"本质上就是一个 turn 里的多个 step。turn/start 与 turn/end 之间的一切(两次请求、一次工具调用)都被日志完整记录。
亲手做实验
实验 1:任务里不带 "echo"
任务改成"随便说点什么"。Mock 适配器的决策条件不满足,没有工具调用,turn 只有一个 step。
实验 2:repeat 参数
改 plugins/mock-adapter.ts,把工具调用的 arguments 改成 {"text":"hi","repeat":3},观察返回内容变三倍。体会 defineTool 的参数校验(args 类型是推导出来的)。
实验 3:让工具失败
在 execute 里 throw new Error('boom')。观察:工具结果变成 isError,日志里 tool/result 带上错误身份,而进程正常退出——失败也是信息。
常见错误
| 现象 | 原因 |
|---|---|
| 模型"不知道"工具存在 | 工具插件没挂载(inject 名写错,fiber 停在 PENDING) |
ToolNotFoundError / UNKNOWN_TOOL | 模型调用了未注册的工具名 |
| 参数校验失败 | 模型传来的参数不符合 parameters schema——这是预期行为,失败会变成结构化错误返回模型 |
下一步
Demo 7 挂上五个扩展点,从"观察"升级到"拦截"。