01 · 主循环全景
沿一条消息走完一次 Agent 循环
点步骤或框图节点逐层聚焦;只有当前环节的数据流会流动。每个节点都对应一个真实源文件。
02 · 上下文工程实验室
亲手拖一拖:对话变长时上下文如何折叠
下面的滑杆完整复刻了 context-pack.ts
里的窗口与触发算法(不是示意,是同一套逻辑):拖动消息条数、切换消息长度,看窗口如何收缩、折叠何时触发、召回何时激活。
模拟一场变长的对话
消息时间线
常量速查 context-pack.ts
| 100k tokens | 每轮上下文总预算(MAX_CONTEXT_TOKENS) |
| 35% | 最近原文最多占预算比例,得出动态窗口 6–20 条 |
| 55% 或 >20 条 | 任一命中即触发折叠:token 估算超预算 55%,或落库消息超 20 条 |
| 8s / 8000 字 | 摘要超时与长度上限;超时不阻塞回答,下轮再试 |
常量速查 context-recall.ts
| top 4 · ≥0.25 | pgvector 余弦相似度召回条数与阈值,排除窗口内消息 |
| 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 步 / 90s | maxSteps 与硬超时;超时返回 tool_timeout 而不是挂死主循环 |
| depth ≤ 2 | 默认 maxDepth=1 允许一层再委派,硬上限 2;超限返回 subagent_depth_exceeded |
| 提示词分叉 | depth < maxDepth 时才告知「可以再拆」;到顶时明令禁止再 spawn |
并行扇出 spawn_research_fanout
多路独立子问题各派一个研究子代理,Promise.all 并行后聚合。
| ≤ 3 路 | tasks 数量上限,zod 层直接锁死 |
| depth=0 / maxDepth=0 | fanout 工人强制不许再嵌套——防「并行 × 深度」组合爆炸 |
| 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
- 注册表先扣牌:装配工具时 risk=dangerous 的工具直接跳过——模型的工具列表里根本没有 delete_*。
- 模型只能出卡:想删东西只能调 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)。
- 前端渲染确认卡:卡片是流里的一个工具 part,带 title / description / destructive 样式。
- 用户点确认:前端把 { confirmed: true, confirmationId } 写回该工具 part 的 result,发起下一轮请求。
- 运行时代执行:新一轮请求进来,运行时扫描消息找到「confirmed 且没有 executionOutcome」的卡,再次校验白名单与注册表后由运行时亲自执行——不经过模型。
- 结果写回: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-run | failStreak 随 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 工具永远不出现在模型的工具列表里,只能经确认闸门由运行时执行。
设计边界(known limits)
- 意图路由是纯正则启发式——省钱且可测,但改写措辞可能路由不中;契约固定返回形状,日后可换小模型实现。
- token 估算是「字符数 ÷ 2」的粗估,只用于触发阈值,不做精确计费。
- 向量召回依赖用户配置了嵌入模型;没配就静默跳过,长对话只能靠摘要兜底。
- 熔断状态 per-run 隔离:同一会话下一条消息会重新给失败工具机会,这是刻意选择而非遗漏。
- 危险操作白名单与域/工具形状由 roadmap 契约锁定(
chat-first-universal-agent4.2–4.4),改动需先过 roadmap update。