RETURN_TO_INDEX

RESEARCH_ENTRY // 工程实践

为智能体设计类型安全的工具协议

用 TypeScript 定义可校验、可观测、可演进的 Tool Calling 接口。

工具协议不是函数列表

模型调用外部能力时,参数错误、权限越界和返回值漂移都会进入系统链路。工具协议需要比普通函数签名包含更多运行时信息。

普通函数由可信代码调用,而 Agent 工具的调用者是概率模型。它可能漏字段、使用旧参数、把字符串当数字,甚至尝试访问描述中未开放的能力。因此,工具边界既是类型边界,也是安全边界和可观测性边界。

需要被显式定义的内容

类型系统负责开发期约束,运行时 Schema 负责处理模型产生的不可信输入。两者结合,才能让工具层在持续扩展时保持稳定。

用同一份 Schema 驱动类型与校验

以 Zod 为例,先定义运行时 Schema,再从中推导 TypeScript 类型,避免接口和校验规则分别维护。

import { z } from 'zod';

const CreateIssueInput = z.object({
  repo: z.string().regex(/^[\w.-]+\/[\w.-]+$/),
  title: z.string().min(1).max(120),
  body: z.string().max(20_000).default(''),
  labels: z.array(z.string()).max(10).default([]),
});

type CreateIssueInput = z.infer<typeof CreateIssueInput>;

const parsed = CreateIssueInput.safeParse(modelArguments);
if (!parsed.success) {
  return {
    ok: false,
    code: 'INVALID_ARGUMENTS',
    details: parsed.error.flatten(),
  };
}

错误应返回结构化代码和可修正字段,而不是一段堆栈文本。模型可以根据 INVALID_ARGUMENTS 修正一次;数据库断开则依据 retryable 决定是否退避重试。

统一结果信封

每个工具都返回一致的外层结构,业务数据放在 data 中。这样执行器无需猜测异常格式,也更容易记录指标。

type ToolResult<T> = {
  ok: boolean;
  data?: T;
  error?: {
    code: string;
    message: string;
    retryable: boolean;
  };
  meta: {
    traceId: string;
    durationMs: number;
    version: string;
  };
};

返回给模型的内容应精简、稳定且有上限。大文件、长日志和二进制数据只返回摘要与受控引用,避免一次工具调用挤满上下文。

副作用、幂等与审批

我会把工具分成读取、写入和破坏性三类。读取工具通常可以自动执行;写入工具展示变更摘要;删除、付款、发布等破坏性操作必须得到明确确认。

所有可重试的写操作都需要幂等键。执行器超时不代表下游没有成功,盲目重试可能创建重复资源。工具还应返回外部系统的资源 ID,便于查询真实状态或执行补偿。

版本演进

不要在原字段上悄悄改变语义。新增可选字段通常可以保持兼容;删除字段、收紧枚举或改变单位时,应发布新版本,例如 create_issue.v2,并在一段时间内同时支持旧调用。

工具描述同样属于协议。描述要说明适用场景、不适用场景、重要约束和副作用,但不要塞入与调用无关的长文档。可以用一组固定任务做回归测试,观察模型是否仍选择正确工具并生成合法参数。

最小审计记录

一次工具调用至少记录调用者、会话、工具版本、参数摘要、审批人、开始/结束时间、结果状态和外部资源 ID。敏感字段在进入日志前脱敏,原始密钥永不进入 trace。

类型安全不是让模型永远不犯错,而是让错误在边界处被识别、解释和阻止,而不是悄悄扩散到真实系统。