开发一个 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工具注册表服务完全就绪后才激活当前插件; - 类型安全的参数 DSL:
defineTool自动为execute(args)提供 TypeScript 类型推导,并在运行时过滤模型可能产生的幻觉参数; - 观测渲染解耦:
execute专注返回干净的业务数据结构,output.render专注将其渲染为最利于大模型理解的高信噪比 Prompt 格式。
步骤一:编写自定义代码检查工具
创建 custom-plugins/lint-tool/src/index.ts 文件:
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 中追加该工具插件:
mode: standard
plugins:
# 挂载本地代码质量扫描工具
- name: "./custom-plugins/lint-tool"
# 基础操作系统工具集
- name: "@deepseek-ai/dsh-plugin-bash"
- name: "@deepseek-ai/dsh-plugin-str-replace"
步骤三:启动 Web 控制台并测试交互
执行热重载启动命令:
pnpm dsh web --patch ./cordis.yml
打开浏览器控制台,向 DeepSeek-V3 / R1 发送提示词指令:
“请帮我分析以下代码是否存在安全风险:
const token = 'sk-123456'; eval(userInput);,请调用质量检查工具。”
运行时跟踪观测:
- 大模型识别到需要执行静态代码审计,自动构造 Tool Call 载荷;
- Harness 拦截器执行 Schema 校验,确认参数类型合法;
execute函数高效运行,捕获eval()与硬编码凭证;output.render格式化后回填给模型,模型据此生成最终的修复建议。
高性能 Tool 设计的四大军规
- 精准的 Prompt 描述:在
description中明确工具的边界与禁止使用的场景,能大幅减少大模型的幻觉式误调用; - 拒绝盲目全量输出:对于返回大量日志或文件内容的工具,应在
render中实现分页、截断或哈希摘要,保护有限的上下文窗口; - 结构化错误返回:当工具执行出错(如文件不存在或编译失败)时,返回明确的错误原因与建议操作,引导模型自发进行下一轮修正;
- 幂等性与安全性:对于带有破坏性写操作的工具,务必配合沙箱隔离或用户确认机制。
常见问题解答 (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({ ... })) 即可同时注入多个功能关联的工具集合。