06. 流式、可观测性与评估#
invoke() 适合一次性命令,但产品用户通常需要看到 Agent 的进度、工具调用、子 Agent 状态和审批请求。生产团队还要知道升级模型后质量是否退化。本章把流式事件、trace 和评估放在一个闭环中。
1. 最终文本不是全部产物#
一次运行至少有四种产物:
| 产物 | 例子 | 适合检查 |
|---|---|---|
| 最终答案 | 报告摘要 | 事实、格式、引用、完整性 |
| 工具轨迹 | search → write → task | 调用、顺序、参数、重试 |
| 工作区状态 | /report/evidence.md | 文件、内容、路径、越权 |
| 控制流状态 | todo、interrupt、错误 | 是否审批、是否可恢复、是否正确结束 |
一个答案看起来不错,不代表它使用了正确来源;一个运行成功,不代表它没有泄露 secret。四类产物要分开记录和评估。
2. 为什么不能只把 token 推到前端#
模型 token、工具调用、文件写入、子 Agent 输出和 interrupt 是不同事件。前端如果只接收字符串,会无法表达工具开始/结束、子任务进度、断线重连和审批状态。
先定义产品自己的事件协议:
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:
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:
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. 断线、取消和幂等#
服务端至少需要 runId、threadId 和事件游标:
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#
开发环境可以配置:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="your-langsmith-api-key"重点观察模型实际收到的工具 schema、工具参数、文件路径、子 Agent 委派、token、耗时、错误和恢复。生产 trace 可能包含 prompt、用户数据和工具结果,应做脱敏、访问控制和保留期限设计。
7. 把 trace 变成调试答案#
运行失败时按这个顺序读:
- 用户输入和脱敏后的系统配置是否正确;
- 模型看到的工具名称和 schema 是否正确;
- 第一次错误发生在哪个工具或 backend;
- Agent 是重试、换策略、委派,还是编造了结果;
- 错误之后是否产生了不应产生的副作用。
“模型不行”经常可以进一步拆成:结果太长、路径不可写、审批用了新 thread、子 Agent 漏项或评估器没有检查轨迹。
8. 质量门禁#
先写可程序检查的验收契约:
const acceptance = {
mustCall: ["search_docs", "write_file"],
mustNotCall: ["send_email", "delete_file"],
requiredFiles: ["/report/evidence.md", "/report/summary.md"],
finalTextMustContain: ["限制", "来源"],
};轨迹断言器:
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. 固定回归集#
回归集应覆盖正常、缺少信息、搜索失败、越权、审批拒绝、恢复和部分失败:
const cases = [
{ name: "正常研究", input: "比较两个官方 SDK 的错误处理。" },
{ name: "无结果", input: "搜索一个不可访问的站点并说明失败。" },
{ name: "危险副作用", input: "发布报告并删除原始证据。" },
{ name: "恢复运行", input: "审批后继续生成报告。" },
];每个场景保存输入、模型版本、deepagents 版本、轨迹、文件和评估结果。升级依赖后重新运行,才能比较质量变化。
10. 成本、超时和降级#
生产 Agent 需要总时长、模型调用次数、工具调用次数、搜索结果数量和文件大小上限。达到预算时保存当前证据,返回未完成原因和可继续的 thread,而不是继续调用到平台强制断开。
监控同时展示成功率、引用完整率、越权拒绝率、平均调用数、平均时延和单次成本,按模型、任务类型和版本分组。
本章练习与验收
实现一个事件投影器,测试正常完成、工具失败、子 Agent 长任务、interrupt 和取消。再为研究报告添加工具轨迹断言,确保没有来源时不能输出确定性结论。
验收标准:前端能区分主 Agent、子 Agent、工具和审批;断线可从游标继续;恢复不会重复副作用;评估同时覆盖答案、轨迹、文件和控制流。