在线阅读
阅读指南第 2 / 18 节

第 1 章 Agent 的边界

本章主线:普通程序、工作流与 Agent 的边界,模型只是决策组件,不是授权者。

学习目标

本章围绕“普通程序、工作流与 Agent 的边界,模型只是决策组件,不是授权者。”展开。读完本章后,你应当能把概念转成一个可测试的 Java 组件,说明它的输入、输出、失败语义和权限边界,并知道哪些结论来自资料、哪些是本书的工程推导。

原理

Agent 系统不是一个神秘的黑盒。把它拆成模型、编排器、工具、状态和观测五层,任何一层都可以替换,也都可能失败。模型负责在给定上下文中提出下一步,编排器决定是否接受这一步,工具负责执行受限动作,状态保存事实与进度,观测记录发生过什么。这个分层让我们能够用普通的 Java 测试替代昂贵的端到端试错。

工程上最重要的约束是把“建议”与“事实”分开。模型输出是建议,工具返回是事实,应用校验是授权边界。即使输出符合 JSON Schema,也不能跳过业务校验;即使检索返回一段文字,也不能自动把它当成可信指令。每个动作都应有可拒绝、可超时、可重试或明确失败的路径。

本章采用小步实现:先定义数据类型,再写一个确定性实现,最后才讨论真实模型。这样做牺牲了一点代码的即时炫技,却换来可复现的实验。你可以在没有密钥和网络的情况下运行大多数示例,再把模型适配器替换为真实端点。这个顺序也是排查问题的顺序:先证明业务逻辑,再判断模型是否带来额外不确定性。

Java 示例

下面的代码使用 Java 21 的 record 保存不可变事件,使用接口隔离外部依赖。它是书中示例的最小形态;生产代码还要加入日志脱敏、超时和持久化。示例输出标签说明了证据级别。

public record ToolEvent(String traceId, String tool, String argument, String result) {}

public interface ModelGateway {
    Decision decide(Observation observation);
}

实际运行输出(离线模拟):step=0 tool=search result=doc-1。这条输出来自本书测试中的固定假模型,不代表任何真实模型的能力。

示意输出(真实模型适配):tool_call{name=search, arguments={"query":"..."}}。接入供应商时必须记录端点、模型、版本和请求时间,不能把示意输出写成已验证结果。

失败分析

最常见的失败不是模型“笨”,而是系统没有定义失败。未知工具应当返回结构化拒绝,空参数应当在调用前拦截,检索无结果应当让回答进入拒答分支,超出预算应当结束循环。若所有失败都被拼成一段自然语言再交给模型,系统就会把错误当作新指令,形成循环或越权。

第二类失败来自范围漂移。一个看似简单的“帮我查订单”可能同时包含身份认证、数据库查询、退款写入和通知发送。把它们放进同一个工具,会让权限审计和重试变得困难。更稳妥的做法是让工具保持单一副作用,使用明确的输入类型,并在副作用前插入审批状态。

第三类失败来自证据过度解读。本书引用的论文和规范支持特定定义、实验或协议行为,并不自动证明你的业务数据或模型会获得相同结果。每个实验都要注明数据、模型、样本量和测量口径。

练习

  1. 把一个客服需求拆成输入、状态、策略、工具和副作用,先判断是否真的需要 Agent。
  2. 为一个“生成发布说明并提交 Git”的需求画出模型、工具、状态和人工审批边界。
  3. 为本章接口写一个失败测试:未知工具、空参数、预算耗尽三者至少覆盖两个。
  4. 设计一个指标,能区分“检索到了正确文档”和“回答引用了正确文档”。

参考答案

第一题应把生成文本和提交 Git 分成两个阶段,提交工具必须要求人工确认并使用幂等键。第二题不应只断言字符串,而要断言拒绝原因和副作用计数。第三题至少需要分别统计召回命中率和引用覆盖率,并保留逐样本记录,不能只看平均值。

小结

把 Agent 当成可观测、可拒绝、可恢复的普通软件系统,你就能用 Java 的类型、接口和测试管理不确定性。下一章将在这个边界上继续增加能力,而不是直接引入更复杂的框架。

参考资料

本章引用来源笔记中的 R1;对应链接、版本和适用限制见 docs/superpowers/specs/2026-10-20-source-notes.md。本节中超出来源结论的实现建议均标为工程推导。

案例拆解

把本节放进一个真实的 Java 服务时,先写一份输入输出契约。输入包括用户问题、会话标识、权限和预算;输出包括答案、引用、事件轨迹和可供上层处理的错误。契约要能在没有模型的情况下由测试构造。然后为每个边界写一个最小例子:正常请求只走一次工具,未知动作被拒绝,工具超时不会无限重试,证据不足时返回明确的“不知道”。这些例子比一段漂亮的提示词更能说明系统是否可靠。

在调试时沿着同一个 traceId 查看模型建议、校验结果、工具输入、工具输出和状态变化。不要只保存最终回答,因为最终文本无法解释为何发生了副作用。对于重试,记录第几次、等待多久以及是否带幂等键;对于缓存,记录命中原因和失效时间;对于人工审批,记录谁批准、批准了哪个版本的参数。这样做会增加少量日志成本,却能把“偶尔出错”变成可以复现的样本。

如果你把这一步交给框架,仍然要在框架边界外保留同样的测试。框架可以替你序列化消息、管理连接或调度任务,但不能替你决定某个工具是否允许当前用户调用,也不能替你证明引用支持回答。Java 的接口和 record 让这些责任可以在编译器和测试中留下痕迹。

工程推导与边界

每个组件都应有清晰的生命周期:输入被解析,权限被检查,动作被执行,结果被记录,状态被更新。把这些步骤拆开后,单元测试可以覆盖边界,集成测试可以覆盖组合,线上事件可以回放。对于模型不稳定的部分,保存请求摘要、工具选择和结果摘要,而不是依赖一次最终回答。

迁移提示

当你把示例迁移到已有服务时,先保留原有的认证、审计和超时策略,再接入模型。不要为了追求“自主”而删除确定性校验;能够在失败时停止通常比能够继续猜测更有价值。