Petrichor 技术可视化 · Assistant Agent Runtime

Agent 架构可视化

拆开 Petrichor 站内 AI 助手的运行时:五域意图路由、上下文折叠与向量召回、按域装配的 30 个工具、三种子代理、危险操作确认闸门、失败降级 playbook 与全量落档。代码都在 src/server/assistant/

← 返回文章列表 GitHub ↗
一句话本质:模型只决定「下一步调用哪个工具」,其余全由运行时接管。

上下文怎么省、工具给不给、危险操作何时真正执行、失败之后怎么办——这些不交给模型自觉, 而是写死在运行时里:预算、阈值、白名单、熔断线,全部是常量。

message → route → pack → loop×8 → confirm → persist

01 · 主循环全景

沿一条消息走完一次 Agent 循环

点步骤或框图节点逐层聚焦;只有当前环节的数据流会流动。每个节点都对应一个真实源文件。

本轮主路径 回传 / 旁路
POST /api/assistant/chat domains[] 注入提示词 按域装配 流式推理 spawn_* 逐步落档 摘要 · 向量读写 危险操作出卡 等用户点确认 UI Message 流式回传 浏览器 · useChat UI Message 流 确认卡渲染 · 压缩状态条 入口网关 requireCurrentUser · focus 403 intent-router 正则打分 · 零 LLM 调用 ContextPack 窗口 6–20 条 · 增量摘要 向量召回 top4 工具注册表 30 个工具 · dangerous 不给模型 Agent 主循环 maxSteps 8 · temp 0.2 注入检测 0.85 · 100k 限流 子代理 research ≤2 层 · fanout ×3 write 只出提案 确认闸门 request_user_confirmation 模型(用户配置) OpenAI · Gemini · DeepSeek… Postgres 落档 thread · run · step message · embedding
交互提示:悬停节点查看职责与源文件;点击节点跳到对应讲解步骤。
键盘: 切步骤 · P 播放 · Esc 退出聚焦

02 · 上下文工程实验室

亲手拖一拖:对话变长时上下文如何折叠

下面的滑杆完整复刻了 context-pack.ts 里的窗口与触发算法(不是示意,是同一套逻辑):拖动消息条数、切换消息长度,看窗口如何收缩、折叠何时触发、召回何时激活。

模拟一场变长的对话

最近窗口(原文保留) 8 条 fixed
折叠触发 未触发
增量摘要 待命
向量召回(折叠区) 待命

消息时间线

✍ 增量摘要(≤8000 字,8s 超时)
窗口内原文 折叠 → 摘要 + 可召回

常量速查 context-pack.ts

100k tokens每轮上下文总预算(MAX_CONTEXT_TOKENS)
35%最近原文最多占预算比例,得出动态窗口 6–20 条
55% 或 >20 条任一命中即触发折叠:token 估算超预算 55%,或落库消息超 20 条
8s / 8000 字摘要超时与长度上限;超时不阻塞回答,下轮再试

常量速查 context-recall.ts

top 4 · ≥0.25pgvector 余弦相似度召回条数与阈值,排除窗口内消息
16 条/批 · ≤500 字折叠区消息按批嵌入,摘录截断后入库
[redacted]入库前清洗:sk-*、api_key=、Bearer、确认卡明文一律脱敏
best effort无嵌入配置、SQLite 环境或查询失败时静默跳过,绝不拖垮回答

03 · 子代理体系

三种子代理,三套不一样的约束

子代理是独立的 Agent 实例(独立提示词、独立 resilience),但共享主 run 落档,步骤名带 spawn_*/d{depth} 前缀,回放时能看清委派链。共同铁律:子代理永远碰不到写入与确认工具

研究子代理 spawn_research_subagent

只读深度检索:search → read → 汇总结论 + 引用(citations 必须来自工具结果)。

domains仅 knowledge / doc_library / system,传入写域直接 400
6 步 / 90smaxSteps 与硬超时;超时返回 tool_timeout 而不是挂死主循环
depth ≤ 2默认 maxDepth=1 允许一层再委派,硬上限 2;超限返回 subagent_depth_exceeded
提示词分叉depth < maxDepth 时才告知「可以再拆」;到顶时明令禁止再 spawn

并行扇出 spawn_research_fanout

多路独立子问题各派一个研究子代理,Promise.all 并行后聚合。

≤ 3 路tasks 数量上限,zod 层直接锁死
depth=0 / maxDepth=0fanout 工人强制不许再嵌套——防「并行 × 深度」组合爆炸
usage 聚合汇总 calls / totalTokens / succeeded / failed,主助手可见成本
部分成功有一路成功即 ok;全挂才报 fanout_all_failed

写规划子代理 spawn_write_subagent

「写意图」先规划后执行:子代理只读检索核实目标,通过独占工具 propose_write_actions 提交提案,自己碰不到数据。

提案白名单仅 6 个工具可提:create / update / share(write)与 delete_article、revoke_share、delete_document(dangerous)
≤ 8 条 · 去重按 toolName+input 去重,白名单外的提案直接丢弃并计数 rejected
risk 标注每条提案带 risk;dangerous 项主助手必须转确认闸门,不得直接执行
6 步 / 90s与研究子代理同规格;无方案且无提案时报 empty_write_plan

04 · 护栏与落档

危险操作、失败降级与全量回放

三层防线的共同思路:不指望模型「自觉」,而是让危险路径在结构上走不通。

确认闸门:删除类操作的六步旅程 confirmation.ts

  1. 注册表先扣牌:装配工具时 risk=dangerous 的工具直接跳过——模型的工具列表里根本没有 delete_*。
  2. 模型只能出卡:想删东西只能调 request_user_confirmation,把目标工具与参数放进 action;toolName 必须命中 7 个白名单(delete_article、revoke_article_share、delete_document、delete_ai_config、update_ai_config_credentials、revoke_agent_api_key、set_public_qa_enabled)。
  3. 前端渲染确认卡:卡片是流里的一个工具 part,带 title / description / destructive 样式。
  4. 用户点确认:前端把 { confirmed: true, confirmationId } 写回该工具 part 的 result,发起下一轮请求。
  5. 运行时代执行:新一轮请求进来,运行时扫描消息找到「confirmed 且没有 executionOutcome」的卡,再次校验白名单与注册表后由运行时亲自执行——不经过模型。
  6. 结果写回:executionOutcome 被 patch 回那条工具消息并记为 step 0,模型看到真实结果后才继续叙述,杜绝「假装已删除」。

失败降级 playbook tool-resilience.ts

工具失败不抛异常,而是返回结构化软失败喂给模型,按同名连败次数升级:

30s单次工具硬超时(Promise.race),超时算一次失败
首败tool_degraded · retry_other_tool:「换工具、调参数,或直接回答」
再败tool_degraded · answer_without_tool:「停止依赖该工具,基于已有信息回答」
连败 2 次tool_circuit_open · 熔断:后续调用直接短路返回,提示词明令不得再调
per-runfailStreak 随 run 生灭,不跨请求记仇;成功一次即清零

全量落档 thread-logic.ts

每一步都可回放,也是后续调优提示词与工具的原始数据:

thread会话 + 上下文摘要 contextSummaryMd 与折叠水位 untilMessageId
run一次请求一条:intentDomains、模型配置、COMPLETED / FAILED 与 errorCode
step每次工具调用:名称(子代理带 spawn_*/d1 前缀)、输出、errorCode、耗时 ms
message用户消息进门即存;助手消息在流结束时剥掉压缩状态 part 后落库
embedding折叠区消息的脱敏摘录 + pgvector 向量,唯一键 message_id
响应头X-Petrichor-Assistant-Thread-Id / Run-Id,前端与日志可互相对账

工具面板:30 个工具,五个域

每轮只装配意图命中域的子集;虚线红 的 dangerous 工具永远不出现在模型的工具列表里,只能经确认闸门由运行时执行。

知识库knowledge
list_knowledge_bases search_knowledge read_knowledge_node
文档库doc_library
list_doc_libraries search_documents read_document
系统system
list_system_overview show_progress show_citations show_data_table upsert_plan save_answer_artifact spawn_research_subagent spawn_research_fanout
内容写入content_write
create_article update_article create_article_share delete_article revoke_article_share delete_document spawn_write_subagent request_user_confirmation
管理面admin
list_ai_configs list_agent_api_keys get_public_qa_setting set_default_ai_config delete_ai_config update_ai_config_credentials revoke_agent_api_key set_public_qa_enabled
read只读 write可直接执行的写入 dangerous仅经确认闸门执行 spawn子代理派发

设计边界(known limits)

  • 意图路由是纯正则启发式——省钱且可测,但改写措辞可能路由不中;契约固定返回形状,日后可换小模型实现。
  • token 估算是「字符数 ÷ 2」的粗估,只用于触发阈值,不做精确计费。
  • 向量召回依赖用户配置了嵌入模型;没配就静默跳过,长对话只能靠摘要兜底。
  • 熔断状态 per-run 隔离:同一会话下一条消息会重新给失败工具机会,这是刻意选择而非遗漏。
  • 危险操作白名单与域/工具形状由 roadmap 契约锁定(chat-first-universal-agent 4.2–4.4),改动需先过 roadmap update。