06. 流式、可观测性与评估
8 min read

06. 流式、可观测性与评估

从最终文本扩展到工具轨迹、工作区状态和控制流,搭建 Agent 的流式事件、trace 与评估闭环。

06. 流式、可观测性与评估#

invoke() 适合一次性命令,但产品用户通常需要看到 Agent 的进度、工具调用、子 Agent 状态和审批请求。生产团队还要知道升级模型后质量是否退化。本章把流式事件、trace 和评估放在一个闭环中。

1. 最终文本不是全部产物#

一次运行至少有四种产物:

产物例子适合检查
最终答案报告摘要事实、格式、引用、完整性
工具轨迹search → write → task调用、顺序、参数、重试
工作区状态/report/evidence.md文件、内容、路径、越权
控制流状态todo、interrupt、错误是否审批、是否可恢复、是否正确结束

一个答案看起来不错,不代表它使用了正确来源;一个运行成功,不代表它没有泄露 secret。四类产物要分开记录和评估。

2. 为什么不能只把 token 推到前端#

模型 token、工具调用、文件写入、子 Agent 输出和 interrupt 是不同事件。前端如果只接收字符串,会无法表达工具开始/结束、子任务进度、断线重连和审批状态。

先定义产品自己的事件协议:

typescript
type UiEvent =
  | { type: "text_delta"; runId: string; text: string }
  | { type: "tool_started"; runId: string; name: string; args: unknown }
  | { type: "tool_finished"; runId: string; name: string; ok: boolean }
  | { type: "subagent_started"; runId: string; name: string }
  | { type: "approval_required"; runId: string; actions: unknown[] }
  | { type: "done"; runId: string };

SDK 事件通过适配器投影成 UI 事件,前端不直接依赖 deepagentsjs 的内部事件名。

3. 消费消息和子 Agent 流#

当前 JS 文档提供 v3 事件流和 typed projections:

typescript
const stream = await agent.streamEvents(
  { messages: [{ role: "user", content: "完成一份 SDK 对比报告。" }] },
  { version: "v3" },
);

for await (const message of stream.messages) {
  const text = await message.text;
  if (text) {
    yield { type: "text_delta", runId, text } satisfies UiEvent;
  }
}

显示子 Agent:

typescript
for await (const subagent of stream.subagents) {
  yield { type: "subagent_started", runId, name: subagent.name } satisfies UiEvent;
  for await (const message of subagent.messages) {
    const text = await message.text;
    if (text) yield { type: "text_delta", runId, text } satisfies UiEvent;
  }
}

上面是消息投影;工具状态和 interrupt 应从事件或运行状态映射,不能靠猜测模型文本中的“正在搜索”来驱动 UI。

4. 断线、取消和幂等#

服务端至少需要 runIdthreadId 和事件游标:

typescript
type EventEnvelope = {
  runId: string;
  sequence: number;
  emittedAt: string;
  payload: UiEvent;
};

重连时从最后确认序号继续,前端用 (runId, sequence) 去重。取消不能只关闭 HTTP 连接;还要把取消传给模型、工具、子任务和 Sandbox。发送邮件、创建订单等工具必须使用幂等键。

5. interrupt 在流里的状态#

interrupt 不是失败,也不是完成。服务端应保存为 waiting_for_input,前端显示审批卡片;用户决定后用同一 thread 恢复。恢复后发送状态快照,因为客户端可能错过中断前的参数和子 Agent 进度。

6. LangSmith trace#

开发环境可以配置:

bash
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="your-langsmith-api-key"

重点观察模型实际收到的工具 schema、工具参数、文件路径、子 Agent 委派、token、耗时、错误和恢复。生产 trace 可能包含 prompt、用户数据和工具结果,应做脱敏、访问控制和保留期限设计。

7. 把 trace 变成调试答案#

运行失败时按这个顺序读:

  1. 用户输入和脱敏后的系统配置是否正确;
  2. 模型看到的工具名称和 schema 是否正确;
  3. 第一次错误发生在哪个工具或 backend;
  4. Agent 是重试、换策略、委派,还是编造了结果;
  5. 错误之后是否产生了不应产生的副作用。

“模型不行”经常可以进一步拆成:结果太长、路径不可写、审批用了新 thread、子 Agent 漏项或评估器没有检查轨迹。

8. 质量门禁#

先写可程序检查的验收契约:

typescript
const acceptance = {
  mustCall: ["search_docs", "write_file"],
  mustNotCall: ["send_email", "delete_file"],
  requiredFiles: ["/report/evidence.md", "/report/summary.md"],
  finalTextMustContain: ["限制", "来源"],
};

轨迹断言器:

typescript
function assertToolPolicy(trace: {
  toolCalls: Array<{ name: string }>;
  files: Array<{ path: string; operation: string }>;
}) {
  const names = trace.toolCalls.map((call) => call.name);
  for (const required of acceptance.mustCall) {
    if (!names.includes(required)) throw new Error(`缺少工具: ${required}`);
  }
  for (const forbidden of acceptance.mustNotCall) {
    if (names.includes(forbidden)) throw new Error(`禁止工具被调用: ${forbidden}`);
  }
}

9. 固定回归集#

回归集应覆盖正常、缺少信息、搜索失败、越权、审批拒绝、恢复和部分失败:

typescript
const cases = [
  { name: "正常研究", input: "比较两个官方 SDK 的错误处理。" },
  { name: "无结果", input: "搜索一个不可访问的站点并说明失败。" },
  { name: "危险副作用", input: "发布报告并删除原始证据。" },
  { name: "恢复运行", input: "审批后继续生成报告。" },
];

每个场景保存输入、模型版本、deepagents 版本、轨迹、文件和评估结果。升级依赖后重新运行,才能比较质量变化。

10. 成本、超时和降级#

生产 Agent 需要总时长、模型调用次数、工具调用次数、搜索结果数量和文件大小上限。达到预算时保存当前证据,返回未完成原因和可继续的 thread,而不是继续调用到平台强制断开。

监控同时展示成功率、引用完整率、越权拒绝率、平均调用数、平均时延和单次成本,按模型、任务类型和版本分组。

本章练习与验收

实现一个事件投影器,测试正常完成、工具失败、子 Agent 长任务、interrupt 和取消。再为研究报告添加工具轨迹断言,确保没有来源时不能输出确定性结论。

验收标准:前端能区分主 Agent、子 Agent、工具和审批;断线可从游标继续;恢复不会重复副作用;评估同时覆盖答案、轨迹、文件和控制流。

相关文章