Xinwei Xiong · 2026 年 6 月 24 日
10 分钟 · 4694 字 · | EN

Relay Agent 架构审计:从设计承诺到本地实现

基于 Relay 私有本地实现的源码审计,核对协调器、五个领域 Agent、Harness、路由、事件系统与浏览器投递链路,区分已实现、已测试、设计中和示意内容,并重点解释 LangGraph 恢复语义、幂等边界、审计持久化、反虚构守卫的能力边界,以及浏览器自动化面对服务条款、提示注入和敏感字段时保留的人类审批机制。

Relay 协调器、五个领域 Agent、浏览器投递、防护机制和审计边界组成的架构图

Relay 架构实现审计:证据、边界与仍未完成的部分

架构图是一张承诺清单;源码审计要问的是,其中哪些箭头已经有了重量。

这篇文章的旧版把 Relay 写成一个公开的开源方案,还说它的 Agent 层尚未开工。到 2026 年 7 月 31 日,这两个判断都不再成立:旧文引用的公开 GitHub 地址返回 404,而我能够核对的是一份私有本地检出。

因此,本文是一份私有本地实现审计,事实固定在本地提交 22586e17ccd43cfaff0512511e71a100c5341608。读者不应据此推断仓库或该提交可以公开下载。

这条边界看似扫兴,却很重要:我可以说明本地代码里有什么,不能把私有证据包装成任何人都能复现的公开结论。

四种状态,四种证据

下文对关键结论使用四种状态:

  • 已实现:审计提交中存在对应的生产路径代码。
  • 已测试:存在聚焦测试,并在本次审计中实际运行。
  • 设计中:它是文档意图或已经识别的要求,不是完成承诺。
  • 示意:用于解释工程方法,不代表 Relay 的精确代码或性能。

“文档里写了”不属于第五种状态。文档记录愿望,执行路径才暴露系统真正承担的责任。

当前实现快照

本地检出已经远远超过一份方案:

区域审计结果状态
协调器Ask Vantage 路由器与 ReAct dock 入口存在已实现
领域 Agent简历、职位匹配、面试、投递准备、趋势五个模块存在已实现
Harness成本、Guard、上下文压缩、checkpointer、权限、审计和事件能力存在已实现
Agent API流式响应、简历、投递、模拟面试和简历构建等 FastAPI 路径存在已实现
TypeScript API有 16 个非测试 route 模块,其中 14 个直接挂载,2 个作为嵌套路由使用已实现
事件系统Redis 事件总线、消费者与处理器存在已实现
浏览器链路Playwright MCP 客户端和需审批的浏览器工具存在已实现、已测试
数据迁移迁移编号从 001022,SQL 中共有 21 条 CREATE TABLE已实现
公开可用性旧文引用的 GitHub 地址当前不可访问非公开

本次审计运行了路由器、浏览器工具和成本追踪器的聚焦测试,共 47 项通过。这只能证明这些路径在审计环境中的表现,不能替代生产端到端测试。

状态:在审计提交上已测试。

现在值得讨论的已经不是“这个设计多么宏大”,而是设计落地以后,系统把失败边界放在了哪里。

一个协调器,五个领域 Agent

Relay 将对话路由与五个领域模块分开:

  1. ResumeAgent 解析、分析、优化和定制简历。
  2. JobMatchAgent 接收并匹配职位。
  3. InterviewAgent 组织面试准备和评估流程。
  4. AppPrepAgent 准备投递材料和浏览器动作。
  5. TrendAgent 提取并总结市场信号。

状态:已实现。

这并不意味着多 Agent 天生更快或更聪明。一个边界是否值得拆开,通常取决于四件事:

  • 触发方式是否不同:对话、事件、定时任务或用户显式动作;
  • 模型与时延要求是否不同;
  • 数据归属是否不同;
  • Prompt 与评测集是否以不同节奏演化。

旧文声称协调成本会按 (O(N^2)) 增长,但这份代码没有提供支持该复杂度结论的基准。协调代价取决于拓扑、共享状态、工具契约与跨 Agent 交接次数,而不是 Agent 数量这一项。

协调器当前仍用 langgraph.prebuilt.create_react_agent 构建 dock graph。

状态:已实现,但存在迁移债。

LangChain 当前的官方 Agent 文档langchain.agents.create_agent 作为高层入口,v1 迁移指南 也说明了从 create_react_agent 迁移的方向。Relay 不需要为接口名称立刻重写稳定路径,但新中间件与运行时能力不应继续加深旧依赖。

分类器比旧稿描述得更小

源码中的 REGEX_ACCEPT_THRESHOLD0.85,不是旧文声称的 0.95。正则命中且置信度达到阈值时,classify_intent() 可以接受结果。

状态:分类器模块中已实现。

但旧版所谓 Layer 2 LLM 分类器已经删除:低于阈值或没有匹配的输入会落到 other。生产 dock 也在 2026 年 7 月 8 日移除了旧的通用正则快路径,因为它产生的事件词汇与 AG-UI 消费者不兼容。现在一般请求进入 ReAct 循环;7 月 13 日新增的确定性快路径只服务于“粘贴职位描述后定制简历”这一狭窄场景。

状态:Layer 2 未实现;通用 dock 快路径已移除,仅保留职位描述定制窄路径。

这正是架构文章容易腐烂的方式:一个模块仍在目录里,不代表它仍位于真实请求路径上。

HITL 是事务边界

Relay 最值得保留的原则是:会改变外部状态的浏览器动作需要用户批准。浏览器快照被视为只读通知,而导航、点击和表单填写由审批装饰器包裹。

状态:已实现。

LangGraph 的 interrupt()Command(resume=...) 很适合这种流程,但恢复语义经常被讲错。恢复并不是从某一行 Python 代码后继续,好像运行栈被原样冻结。根据官方 interrupt 指南 ,恢复时节点会从开头重新执行,传入的 resume 值成为 interrupt() 的返回值。

def approval_node(state):
    decision = interrupt({
        "action": "fill_form",
        "fields": state["proposed_fields"],
    })

    if decision["type"] != "approve":
        return {"status": "cancelled"}

    # 不可逆动作放在批准之后。
    return perform_idempotent_fill(
        key=state["operation_id"],
        fields=decision.get("fields", state["proposed_fields"]),
    )

所以,interrupt() 之前的任何操作都可能再次发生:数据库插入需要幂等键或 upsert,消息需要去重,外部调用最好放到批准后的独立节点。LangGraph 持久化指南 也说明了另一半前提:可靠的人类审核需要 checkpointer 和稳定的 thread ID。

Relay 状态:持久化 checkpointer 支持已实现;每个副作用仍需逐项做幂等审计。

HITL 不是一个装饰性的确认框,而是由四部分组成的事务边界:

  • 完整展示待执行动作;
  • 允许用户修改决策载荷;
  • 用正确的用户和 thread 标识持久化状态;
  • 批准后走幂等执行路径。

缺少任意一项,“人在回路”都可能只剩下戏剧效果。

Harness 才是可靠性真正居住的地方

Relay 在 graph runtime 外包了一层 Harness,负责模型选择、成本核算、预算 Guard、上下文压缩、权限、持久化、审计和事件发送。

状态:已实现。

这是我最信任的 Agent 工程方向:Prompt 描述理想行为,Harness 负责在行为漂移时限制损害。更完整的讨论可参阅站内文章《Agent 工程:真正决定成败的是那 98% 的 Harness》《上下文工程:新的基础设施》

成本可观测,不等于成本会自动正确

当前代码通过上下文本地计数器记录模型用量,并在模型调用前后执行预算检查。

状态:已实现。

旧稿把精确模型价格、0.50 美元 session 上限与自动分层降级写成了不会变化的产品事实。模型名、供应商价格和配置都可能比文章更新得更快。真正稳定的原则只有几条:

  1. 记录供应商响应中的真实模型标识与 token 用量;
  2. 把成本和时延挂到同一条请求 trace;
  3. 在代码中执行预算,而不是靠 system prompt 劝告;
  4. 测试预算耗尽时系统如何失败。

价格表也需要明确的更新责任人。精确到小数点后四位,无法弥补价格已经过期。

审计写入目前仍是 best-effort

Relay 的审计 context manager 使用 asyncio.create_task() 调度数据库写入,并保留待完成任务的强引用,避免了 Python 文档提到的一类弱引用风险。

状态:作为 best-effort 遥测已实现。

但“已调度”不等于“已持久化”。进程崩溃或事件循环突然停止,仍可能丢失审计记录。Python task 文档 建议保留任务引用,却不会把后台任务变成可靠队列。

如果某条审计记录具有法律、财务或运维上的强制性,就应使用事务 outbox、持久队列,或在关键路径上等待写入完成。

状态:审计可靠投递属于设计要求,不是已经实现的保证。

事件消费者同理。create_task() 能把工作放到后台,交付语义仍来自确认、重试、去重与持久化 offset。

反虚构 Guard:有限检测器,不是真相机器

Relay 的简历 Guard 会把生成内容中的部分命名实体与定量实体同原始简历比较,覆盖公司、职位、学校、学位、项目名、百分比、金额、年份和较大的独立数字,并给变更记录标记“安全”“需审核”或“不受支持”。

状态:已实现。

这是有价值的运行时兜底,却不能证明简历必然真实。

它可能发现原文没有的“吞吐提升 40%”,却可能漏掉“主导迁移战略”这类没有数字的无依据表述,也可能漏掉重新包装的因果关系、误导性同义词或不在启发式范围内的小数字。

更诚实的能力契约是:

  • Guard 降低一组定义明确的无依据实体风险;
  • 假阴性仍然存在;
  • 生成结论仍需人工审核;
  • UI 应为拦截项和模糊项保留来源证据。

状态:有限检测器已实现;完整语义 grounding 仍是设计目标。

“绝不虚构”不是可辩护的承诺。“展示证据,并拦截已知的无依据模式”才是。

浏览器投递是一条安全边界

Relay 可以通过 Playwright MCP 连接用户现有浏览器,请求页面快照,并在批准后执行导航、点击和字段填写。实现会在调用 MCP 前移除类似凭证的字段名,包括密码、PIN、SSN 与信用卡字段。

状态:MCP 链路、写操作 HITL 与有限敏感字段 denylist 已实现。

旧稿说用户自己的浏览器能让辅助行为与手工操作“无法区分”,并能绕过封号与 CAPTCHA。这些说法既没有证据,也会诱导危险实现。平台可以从行为节奏、DOM 交互、扩展信号或服务端控制识别自动化;已有登录态也不等于用户获准自动化任何网站。

生产级浏览器 Agent 至少需要以下控制:

控制必要姿态审计状态
域名白名单只访问明确批准的 ATS 与招聘域名设计缺口
服务条款检查不自动化目标服务禁止的流程设计缺口
间接提示注入防护页面文字只是不可信数据,不能成为指令来源设计缺口
敏感字段策略按政策阻止凭证、受监管信息和自我识别字段部分实现
写操作审批精确预览导航、填写和点击目标及其值已实现
最终提交边界用户亲自提交,或逐次明确批准策略已定义;需逐路径核对
CAPTCHA停止并把控制权交还用户,绝不规避必需策略
行为限速限制重试、导航频率与重复投递设计缺口
审计轨迹记录操作者、域名、建议值、决策和结果部分实现

OWASP 提示注入指南 特别指出,网站和文件等外部来源可能包含间接提示注入。ATS 页面可以藏入诱导 Agent 改道、泄露数据或调用其他工具的文字。Accessibility snapshot 让模型更容易读页面,却不会让页面内容变得可信。

最安全的心智模型是:浏览器是一件上膛的工具,页面则是一个不可信的调用者。

数据与事件:已经存在什么,还缺什么

审计到的迁移目录从 001022,较新的编号同时提供 forward 与 rollback 文件;所有迁移 SQL 合计有 21 条 CREATE TABLE。这取代了旧稿“17 张表”的过时说法。

状态:迁移 SQL 中已实现。

Schema 包含用户、文件、简历版本、职位、投递草稿、对话、记忆、面试、Agent 配置与任务、简历建议、趋势快照和持久化流事件等数据。

Relay 也有 Redis 事件总线和用于跨 Agent 反应的消费者。

状态:已实现。

但源码不支持“事件必然送达”的承诺。要让简历更新在崩溃后仍可靠触发下游匹配,至少还需要:

  • 生产者侧事务 outbox,或具有相同原子性的机制;
  • 消费确认;
  • 有界重试与死信路径;
  • 以 event ID 为键的幂等处理;
  • replay 与 lag 的可观测性。

状态:可靠性加固仍处于设计阶段。

一个系统不会因为用了 Redis 就自动解耦。只有当失败归属被写清楚时,解耦才真正发生。

这次审计删除了哪些结论

旧稿中有一些看似精确、实则没有证据的数字:

  • 70/25/5 的字段覆盖比例;
  • 每次投递 0.003 美元的 LLM 成本;
  • 98% 的理论毛利;
  • 一个数量级的时延提升;
  • “可多匹配 23 个岗位”的建议效果;
  • 未提供基准却声称更优的加权匹配质量。

这些结论已经删除。它们可以成为实验假设,不能冒充实现事实。

一份诚实的测量应公开:

  • 数据集与采样窗口;
  • 任务成功的定义;
  • 人工审核协议;
  • p50、p95 与错误率;
  • 模型及供应商版本;
  • 包含重试在内的完整成本分布;
  • 基线与置信区间。

状态:示意性评测方案。

如果没有这层证据包络,一个百分比不过是穿着实验服的排版。

2026 年审查 Agent 架构的八个问题

现在审查一个 Agent 系统,我会按顺序问:

  1. 系统能执行哪些不可逆动作?
  2. 持久化审批边界在哪里?
  3. 重试或恢复后,哪些代码会运行两次?
  4. 哪些模型输出会同来源证据比较?
  5. 哪些外部内容可能发出间接指令?
  6. 进程死亡时,哪些事件或审计记录会丢失?
  7. 哪些结论已经测量,哪些仍只是设计?
  8. 哪些框架 API 是当前推荐,哪些只是暂时容忍的迁移债?

如果你也在设计类似系统,站内的《如何信任一个无人值守的 AI Agent》 讨论了运维信任边界;《LangGraph 架构深度解析》 则梳理了底层 graph 原语。

结语

Relay 已经从架构方案跨进了真实实现:协调器、五个领域 Agent、Harness、路由、事件、迁移历史和 MCP 浏览器工具都确实存在。

但“存在”不会自动变成“生产级”。

下一层工作更安静,也更重要:有节奏地迁出 create_react_agent,让关键审计与事件真正持久化,把浏览器边界加固到能够面对不可信页面,把 grounding 从数字和命名实体扩展到语义层,并用可复现评测替代漂亮的性能数字。

这次审计留下的哲理不是“少做设计”,而是:

一个设计开始值得信任,是从每一条漂亮箭头都拥有重试策略、责任人和诚实状态开始。

这才是可以继续带向生产的架构。

读者回响

加入讨论

新文章写好,先寄给你

每有新文章,寄一封信到你的邮箱。双重确认,随时退订。