通过use-case.md和tool-contracts.md在写代码前定义清楚输入输出边界、工具数量和成功指标,防止下游prompt/memory无固定目标可循。
在写任何代码之前,先准备两份文档:docs/use-case.md 和 docs/tool-contracts.md。跳过这一步是团队最终做出一个没人信任的 Agent 的最常见原因——不是因为想法不好,而是因为下游所有环节(evals、prompts、memory)都没有一个固定的靶心可以瞄准。
在写一行代码之前,先填好四个关卡:
工具数量上限比看起来更重要。给一个 Agent 配备 10 个以上工具,它就会开始 hallucinate 工具名称、选错工具——这是认知负担问题,不是依赖问题。将这个 Agent 限制在 5 个工具、准确覆盖三类请求(订单状态、退款、KB 查询),能让每次运行都保持在健康的 3–8 次工具调用范围内,而不会膨胀成一个需要拆分成多个专业 Agent 的系统。(第七部分会讲什么时候该拆分的实际成本计算。)
bounded output 同样重要:resolved / refund_proposed / escalated 不只是文档——它变成了一个可解析的字面前缀(RESOLVED:、REFUND_PROPOSED:、ESCALATED:),Agent 的最终消息必须以此开头。第四部分会展示如何解析这个前缀,第五部分会展示为什么当初只信任这个字符串本身是一个真实的 bug。
这是最容易投入不足的部分。工具的 description 字段不是给未来开发者看的注释——它是 LLM 决定何时调用该工具的唯一依据。把它当作一份规格说明书来对待。
以下是 src/tools/index.ts 中真实的 refund_eligibility 定义:
{
name: "refund_eligibility",
description:
"Check whether an order is eligible for a refund BEFORE ever proposing one. " +
"Orders are eligible only if delivered and within a 30-day window of order_date. " +
"Cancelled orders are already auto-refunded and are never eligible for a manual " +
"refund. Returns eligible, max_amount_usd, and a policy_ref explaining the decision.",
gated: false,
input_schema: {
type: "object",
properties: {
order_id: { type: "string" },
reason: { type: "string", description: "Customer's stated reason for the refund request" },
},
required: ["order_id", "reason"],
},
}
注意 30 天窗口期在工具描述中明确说明,而不是只在 system prompt 里提一句。这不是冗余——这是后续迭代中发现的经验(完整故事在第六部分):只在 system prompt 中存在的策略逻辑在高负载下会被忽略。它必须在工具本身中是承压的(load-bearing)。
完整的合约表,将每个工具与其 schema 和 failure mode 对应:
有三个细节值得特别指出:
幂等性 key 不是可选项。issue_refund 需要它,因为重试会发生——网络抖动、模型在短暂错误后重试——没有幂等性保护的重款工具意味着重试可能导致重复退款。这在工具执行器本身强制执行,而不是假设工具会有良好行为:
// src/tools/issueRefund.ts
const issuedRefunds = new Map<string, string>(); // idempotency_key -> confirmation_id
export async function issueRefund(args: Record<string, unknown>) {
const idempotencyKey = args.idempotency_key ? String(args.idempotency_key) : "";
if (!idempotencyKey) return { error: "idempotency_key_required" };
const existing = issuedRefunds.get(idempotencyKey);
if (existing) return { confirmation_id: existing, replayed: true };
// ... issue the refund, store it under idempotencyKey
}
Failure mode 是结构化的,而非猜出来的。order_lookup 查不到订单时返回 { error: "order_not_found", order_id },而不是一个模型需要自行解释的空对象。kb_search 没有匹配时返回明确的空 articles: [],这样 system prompt 就能说"如果返回空,就 escalate——不要编造答案"。
Gated 工具在合约层面标记。issue_refund 和 send_email 在其定义中带有 gated: true。该标志被策略层(第五部分)读取,以决定哪些工具调用需要在执行前有人在循环中把关——gating 决策从这里开始,从合约开始,而不是在下游某处。
所有五个工具都从内存中的 mock store 读取——src/data/mockData.ts:5 个客户、10 个订单(包含在退款窗口内和窗口外的)、5 篇 KB 文章。这是故意做成的假数据(这个 repo 是一个参考构建,不是生产系统——更多说明见 repo 自己的 README),但 schema 被塑造成镜像真实电商 API 的订单/退货/明细结构,所以后续接入真实后端只是数据层改动,不需要重写工具合约。
Part 3:Build the Eval Set Before the Agent Exists → 会讲如何构建 eval set——21 个 case 在 Agent 循环存在之前就写好了,而且这个顺序本身就是重点,不是可选项。