开发基础

开发一个 Tool

掌握为 Agent 注册自定义工具的标准规范,包含严格输入参数 Schema 校验与结构化错误自愈反馈机制。

在自主智能体(Autonomous Agent)系统中,工具(Tool) 是大语言模型感知外部世界、操作操作系统、检索数据库与修改代码的“实体执行手臂”。

在 DeepSeek Harness 中,Tool 绝非一个简单的异步回调函数,而是集成了 DSL 参数声明运行时类型守卫模型可读观测渲染管道(Observation Rendering Pipeline) 的高可靠执行单元。

本篇教程将指导你使用 @deepseek-ai/dsh-tools 提供的 defineTool DSL,构建一个生产级别的 “代码复杂度与安全规范扫描工具(Code Quality & Lint Inspector)”,并在 Web UI 中完成实机联调。


核心架构:defineTool 与依赖注入机制

Harness 的工具系统通过 @deepseek-ai/dsh-tools 实现了模型层与底层执行逻辑的彻底解耦:

┌─────────────────────────────────────────────────────────────┐
│                 DeepSeek 工具调用执行流水线                  │
├─────────────────────────────────────────────────────────────┤
│  1. 大模型发起 Tool Call:{ name: 'inspect_code', args: ... }│
│                         ▼                                   │
│  2. defineTool 参数校验:按 JSON Schema 进行类型与边界截断  │
│                         ▼                                   │
│  3. execute(args):执行具体业务逻辑并返回结构化数据对象     │
│                         ▼                                   │
│  4. output.render:将结果转换为标准模型消息块 [{ type: 'text'}]│
│                         ▼                                   │
│  5. 结构化观测数据(Observation)注入上下文,进入下一轮推理 │
└─────────────────────────────────────────────────────────────┘
  • 声明式依赖注入:通过 export const inject = ['tools'],Cordis 微内核确保在 ctx.tools 工具注册表服务完全就绪后才激活当前插件;
  • 类型安全的参数 DSLdefineTool 自动为 execute(args) 提供 TypeScript 类型推导,并在运行时过滤模型可能产生的幻觉参数;
  • 观测渲染解耦execute 专注返回干净的业务数据结构,output.render 专注将其渲染为最利于大模型理解的高信噪比 Prompt 格式。

步骤一:编写自定义代码检查工具

创建 custom-plugins/lint-tool/src/index.ts 文件:

typescript
import type { Context } from '@deepseek-ai/cordis';
import { defineTool } from '@deepseek-ai/dsh-tools';

// 1. 声明插件标识与服务依赖
export const name = 'code-inspector-plugin';
export const inject = ['tools'];

export function apply(ctx: Context) {
    // 2. 向 Harness 全局工具库注册自定义 Tool
    ctx.tools.register(defineTool({
        // 工具唯一标识(与模型 Tool Call 协议中的 name 字段严格对齐)
        name: 'inspect_code_quality',

        // 语义描述:明确告知模型“何时调用该工具”以及“预期的输入输出”
        description: '静态分析指定代码片段的圈复杂度、代码行数及潜在安全隐患(如硬编码密钥或高危函数)。',

        // 输入参数模式定义
        parameters: {
            code: {
                type: 'string',
                required: true,
                description: '待检测的源码文本内容(支持 TypeScript / JavaScript / Python)'
            },
            maxAllowedLines: {
                type: 'number',
                required: false,
                description: '单函数建议最大行数阈值,默认为 50'
            },
            strictSecurityCheck: {
                type: 'boolean',
                required: false,
                description: '是否开启严格的安全规范检查,默认为 true'
            }
        },

        // 输出协议定义与模型消息渲染器
        output: {
            schema: { type: 'object' },
            // render 函数将 execute 返回的 raw 数据转换为模型上下文消息
            render: (_args, result) => [
                {
                    type: 'text',
                    text: `[代码扫描报告]
` +
                          `• 总行数: ${result.totalLines}
` +
                          `• 疑似高危关键词: ${result.vulnerabilities.length > 0 ? result.vulnerabilities.join(', ') : '无'}
` +
                          `• 规范评估: ${result.passed ? ' 检查通过' : ' 存在需要重构的潜在风险'}`
                }
            ]
        },

        // 具体执行逻辑
        async execute(args) {
            const lines = args.code.split('\n');
            const vulnerabilities: string[] = [];

            if (args.strictSecurityCheck !== false) {
                if (args.code.includes('eval(')) vulnerabilities.push('禁止使用 eval() 执行动态脚本');
                if (args.code.includes('password =') || args.code.includes('api_key =')) {
                    vulnerabilities.push('检测到疑似硬编码凭证');
                }
            }

            const maxLines = args.maxAllowedLines || 50;
            const passed = vulnerabilities.length === 0 && lines.length <= maxLines;

            return {
                totalLines: lines.length,
                vulnerabilities,
                passed
            };
        }
    }));
}

步骤二:在 cordis.yml 中挂载并启用工具

在工作区根目录的 cordis.yml 中追加该工具插件:

yaml
mode: standard

plugins:
  # 挂载本地代码质量扫描工具
  - name: "./custom-plugins/lint-tool"

  # 基础操作系统工具集
  - name: "@deepseek-ai/dsh-plugin-bash"
  - name: "@deepseek-ai/dsh-plugin-str-replace"

步骤三:启动 Web 控制台并测试交互

执行热重载启动命令:

bash
pnpm dsh web --patch ./cordis.yml

打开浏览器控制台,向 DeepSeek-V3 / R1 发送提示词指令:

“请帮我分析以下代码是否存在安全风险:const token = 'sk-123456'; eval(userInput);,请调用质量检查工具。”

运行时跟踪观测

  1. 大模型识别到需要执行静态代码审计,自动构造 Tool Call 载荷;
  2. Harness 拦截器执行 Schema 校验,确认参数类型合法;
  3. execute 函数高效运行,捕获 eval() 与硬编码凭证;
  4. output.render 格式化后回填给模型,模型据此生成最终的修复建议。

高性能 Tool 设计的四大军规

  1. 精准的 Prompt 描述:在 description 中明确工具的边界与禁止使用的场景,能大幅减少大模型的幻觉式误调用;
  2. 拒绝盲目全量输出:对于返回大量日志或文件内容的工具,应在 render 中实现分页、截断或哈希摘要,保护有限的上下文窗口;
  3. 结构化错误返回:当工具执行出错(如文件不存在或编译失败)时,返回明确的错误原因与建议操作,引导模型自发进行下一轮修正;
  4. 幂等性与安全性:对于带有破坏性写操作的工具,务必配合沙箱隔离或用户确认机制。

常见问题解答 (FAQ)

Q1: 为什么自定义工具插件必须声明 inject = ['tools']

解答:因为 ctx.tools 是由 @deepseek-ai/dsh-tools 核心服务包注入的单例注册中心。声明 inject 可以让微内核在加载期建立正确的拓扑顺序,避免因异步加载顺序导致 ctx.tools 为空时调用报错。

Q2: 为什么不直接在 execute 中拼接好最终字符串,而要通过 output.render 处理?

解答execute 的职责是生产结构化的原生业务数据(便于自动化测试与日志持久化),而 output.render 则专注于将数据转换为大模型最易理解的文本或多模态消息块。这种关注点分离极大提升了工具的复用性。

Q3: 如果模型传入了 parameters 中未声明的多余参数,系统会如何处理?

解答defineTool 底层基于 JSON Schema 规范进行严格过滤。未声明的多余参数会被安全拦截,并向模型返回清晰的 Schema 校验失败信息,促使模型在下一次推理中纠正调用格式。

Q4: 可以在一个插件的 apply 函数中同时注册多个不同的工具吗?

解答:完全可以。只需在一个 apply(ctx) 中多次调用 ctx.tools.register(defineTool({ ... })) 即可同时注入多个功能关联的工具集合。