Skip to content

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:让工具失败

executethrow new Error('boom')。观察:工具结果变成 isError,日志里 tool/result 带上错误身份,而进程正常退出——失败也是信息

常见错误

现象原因
模型"不知道"工具存在工具插件没挂载(inject 名写错,fiber 停在 PENDING)
ToolNotFoundError / UNKNOWN_TOOL模型调用了未注册的工具名
参数校验失败模型传来的参数不符合 parameters schema——这是预期行为,失败会变成结构化错误返回模型

下一步

Demo 7 挂上五个扩展点,从"观察"升级到"拦截"。

基于 DeepSeek Harness(开发者预览版 0.1.0-rc.6)与 Cordis 撰写