Petrichor 技术可视化 · Assistant Agent Runtime

Agent 架构可视化

拆开 Petrichor 站内 AI 助手的运行时:规则 + LLM 两段式意图路由、核心域常驻装载、上下文折叠与工具结果压缩、按需展开的 Skills、36 个工具、三种子代理、确认闸门与会话 allowlist、失败降级 playbook、全站星图检索,以及只给操作员的三层记忆。代码都在 src/server/assistant/src/server/site-graph/

一句话本质:模型只决定「下一步调用哪个工具」,其余全由运行时接管。

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

message → route → pack → loop×12/20 → confirm → persist

01 · 主循环全景

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

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

本轮主路径 回传 / 旁路
POST /api/assistant/chat domains · confidence 并行 摘要 + 召回注入 核心域常驻 流式推理 spawn_* 逐步落档 摘要 · 向量读写 危险操作出卡 等用户点确认 UI Message 流式回传 星图检索 浏览器 · useChat UI Message 流 意图芯片 · 压缩条 确认卡 · 步数预算 入口网关 requireCurrentUser · focus 403 意图路由(两段) 正则打分 → LLM 复核 阈值 0.5 · 超时 5s ContextPack tool 结果压缩 · 窗口 6–20 增量摘要 · 向量召回 top4 工具注册表 + Skills 36 个工具 · 目录进提示词 Agent 主循环 maxSteps 12 / 20 · temp 0.2 注入检测 0.85 · 100k 裁剪 单工具 30s 超时 子代理 research ≤2 层 · fanout ×3 write 只出提案 确认闸门 request_user_confirmation 服务端票据 · 会话 allowlist 模型(用户配置) OpenAI · Gemini · DeepSeek… Postgres 落档 thread · run · step message · embedding confirmation 票据 全站星图 公开载荷 · 缓存 1 天 限跳 BFS ≤3 跳
交互提示:悬停节点查看职责与源文件;点击节点跳到对应讲解步骤。
键盘: 切步骤 · P 播放 · Esc 退出聚焦

02 · 两段式意图路由

规则先手,只在拿不准时才花一次 LLM

下面的输入框完整复刻 intent-router.ts 的五域正则打分(同一套正则、同一套权重): 打字看分数怎么涨、主域怎么选、辅助域怎么补、置信度到多少才会触发 intent-llm.ts 的复核。

输入一句话试试

五域打分

意图域(含辅助补域)
置信度
是否触发 LLM 复核
实际装载域 / 步数

rationale:—

规则打分 intent-router.ts

+3content_write:新建/创建/写入/删除/移动/迁移/跨库/重命名/归档/发布/撤销分享/改标题/更新正文…
+3admin:模型配置 / ai 配置 / 密钥 / api key / 公开问答 / 吊销 / 配额 / 开关
+2knowledge:知识库 / 文章 / wiki / 笔记
+2doc_library:文档 / 文件 / pdf / word / excel / csv
+2system:多少 / 几个 / 统计 / 概览 / 状态 / 总数 / 系统 / 是否就绪
+4focus 指着的实体所属域(FOCUS_DOMAIN_BOOST
+1本会话最近用过的每个工具,按其注册域加分
取前 2MAX_PRIMARY_INTENT_DOMAINS:主域最多 2 个,辅助域另计
置信度min(0.9, 0.5 + 信号数 × 0.1);无信号回退只读三域,0.3

LLM 复核 intent-llm.ts

只在两种情况下才花这一次调用:置信度 < 0.5,或规则命中 admin

generateObjectzod schema 锁死 { domains(1–2), confidence, rationale ≤80 字 },temperature 0
5s 超时AbortSignal.timeout 与请求 signal 取 any;超时/失败/中断静默回退规则结果
admin 保底规则命中 admin 时 LLM 不得剥掉(mergeAdminDomainFromRules);LLM 失败也保留,rationale 追加 admin-domain-kept-*
admin 粘性上一轮 run 的意图域含 admin 时本轮继续挂载;content_write 已常驻,不再做粘性
并行意图复核与 ContextPack 用 Promise.all 并行,不串行阻塞首 token
确认 resume只回传确认、未追加新 user 消息时整段跳过(rationale 追加 confirmation-resume

意图 ≠ 装载 domain-types.ts

对齐 Claude Code:小工具集常驻,不靠意图硬门控藏工具。意图芯片仍展示路由结果,但实际装载走另一条:

常驻四域system · knowledge · doc_library · content_write,每轮都装
按需一域admin 只在意图命中时装载
补域规则knowledge/doc_library → 补 system;admin → 补 content_write(要用确认工具)
步数按意图意图含 content_write 或 admin → maxSteps 20,否则 12——用意图芯片而非常驻域,避免永远按写档给步数
步数预算条工具调用数达 maxSteps - 1 推 warning(不落库);用尽推 exhausted(落库,刷新后仍提示续跑)

03 · 上下文工程实验室

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

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

模拟一场变长的对话

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

消息时间线

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

窗口与折叠 context-pack.ts

100k tokens每轮上下文总预算 MAX_CONTEXT_TOKENS
35%最近原文最多占预算比例,逐条试算得出动态窗口 6–20 条
fixed / token_budget / turn_budget窗口收敛原因三态:触下限 6 / 卡 token / 触上限 20
55% 或 >20 条任一命中即触发折叠:token 估算超预算 55%,或落库消息超 20 条
字符数 ÷ 2token 粗估:JSON.stringify(messages).length / 2,只用于触发阈值
8s / 8000 字摘要超时与长度上限;超时不阻塞回答,下轮再试
24000 字送进压缩器的折叠区转写上限 buildFoldableTranscript
水位成功后写 contextSummaryUntilMessageId(排除最近 N 条后的最大 message.id)

工具结果压缩 compactToolParts

折叠之前先做一层更便宜的裁剪:只有最近 2 个用户轮次的 tool 结果保留全文,更早的压成一行。

keepRecentTurns 2从后往前数 2 个 role=user 的下标作为截断点
保留字段toolNameokerrorCode,以及 id 类字段:id / articleId / documentId / knowledgeBaseId / libraryId / confirmationId / planId / shareId / configId
message 截断没有 id 时退而保留 messageerror 前 120 字
顺序压缩 → 算窗口 → 切分 → 折叠摘要;窗口试算基于压缩后的消息,省下的 token 直接换成更多原文

向量召回 context-recall.ts

top 4 · ≥0.25pgvector 余弦相似度召回条数与阈值,排除窗口内消息 id
激活条件落库消息数 > 窗口条数即激活(与折叠是否触发无关)
16 条/批 · ≤500 字缺向量的历史消息在后台异步补齐,不阻塞本轮召回
[redacted]入库前清洗:sk-*api_key=、Bearer、确认卡明文一律脱敏
best effort无嵌入配置、SQLite 环境或查询失败时静默跳过,绝不拖垮回答

向量这条线还有一整套自己的故事,单独拆在 《向量化 · 入库索引 · 召回》

04 · 工具装配与 Skills

36 个工具常驻四域,playbook 按需展开

工具列表要小而稳,业务流程要长而细——两者矛盾,于是拆开:工具常驻装载skill 只把目录塞进系统提示词,模型自己判断要不要 load_skill 取全文。

🔒 仅操作员装载

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

核心四域每轮常驻,admin 按意图装载; 虚线红 的 dangerous 工具永远不出现在模型的工具列表里,只能经确认闸门由运行时执行; 带 🔒 的 requiresOperator 工具只装给操作员,其余用户完全看不到(详见第 07 节)。

知识库knowledge常驻
list_knowledge_bases search_knowledge search_knowledge_graph 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 load_skill save_answer_artifact spawn_research_subagent spawn_research_fanout search_operator_history memory_manage
内容写入content_write常驻
request_user_confirmation create_article update_article preview_article_update create_article_share move_article delete_article revoke_article_share delete_document spawn_write_subagent
管理面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只读 graph星图检索(本次新增) write可直接执行的写入 dangerous仅经确认闸门执行 spawn子代理派发 operator仅操作员装载

Skills 渐进披露 skills/index.ts · load_skill

四个内置 playbook,按装载域过滤后只把「名字 + 一句话」写进系统提示词,正文要用才取:

knowledge-qa知识库/文章问答与引用(域:knowledge)
doc-library-qa文档库检索与片段定位(域:doc_library)
article-write写改/移动文章、分享与危险删除(域:content_write)
admin-ops模型配置 / API Key / 公开问答(域:admin)
load_skill按 name 取全文;未命中返回 skill_not_found 并附可用清单
操作员覆盖同名操作员 skill 覆盖内置;该表现已只读(写入路径随面板移除)

系统提示词怎么拼 system-prompt.ts

提示词是按本轮装载域拼出来的,不是一份写死的长文:

  1. 操作员记忆段(若是操作员,见第 07 节)拼在最前
  2. 身份 + 「本轮装载域:…只调用本轮实际提供的工具」
  3. 按域追加指引:system → 进度/计划/子代理/记忆;knowledge → 图片渲染、跨库检索要带 hits[].knowledgeBaseId、关联型问题先走星图;content_write → 移动用 move_article、大改先 preview;admin/content_write → 危险操作必须出确认卡
  4. skill 目录清单
  5. 兜底铁律:以工具结果为准、软失败按 action 换招、熔断工具不再调、只用中文
  6. 末尾接上下文摘要与向量召回片段(buildInstructionsWithContextExtras

05 · 子代理体系

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

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

研究子代理 spawn_research_subagent

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

domains仅 knowledge / doc_library / system,传入写域直接 400
6 步 / 90smaxSteps 与硬超时;超时返回 tool_timeout 而不是挂死主循环
depth ≤ 2默认 maxDepth=1 允许一层再委派,硬上限 2;超限返回 subagent_depth_exceeded
提示词分叉depth < maxDepth 时才告知「可以再拆」;到顶时明令禁止再 spawn
贯通取消主请求 AbortSignal 透传;用户点停止时子代理一并中断,errorCode=aborted

并行扇出 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 提交提案,自己碰不到数据。

提案白名单7 个工具可提:create_article / update_article / create_article_share / move_article(write)与 delete_article / revoke_article_share / delete_document(dangerous)
≤ 8 条 · 去重toolName + JSON(input) 去重,白名单外的提案直接丢弃并计数 rejected
risk 标注每条提案带 risk;dangerous 项主助手必须转确认闸门,不得直接执行
只读三域装载 knowledge / doc_library / system,另外挂上独占的 propose 工具
6 步 / 90s与研究子代理同规格;无方案且无提案时报 empty_write_plan

06 · 护栏与落档

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

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

确认闸门:删除类操作的七步旅程 confirmation.ts · confirmation-store.ts

  1. 注册表先扣牌:装配工具时 risk=dangerous 的工具直接跳过——模型的工具列表里根本没有 delete_*。就算硬调,注册表里的 execute 也会先撞 directDangerousGuard
  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. 服务端签票:校验通过后往 petrichor_assistant_confirmation 写一张 pending 票据,存的是服务端认定的 toolName 与 input;同 key 重提则覆盖。
  4. 前端渲染确认卡:卡片是流里的一个工具 part,带 title / description / destructive 样式,以及「本会话允许同类操作」勾选框。
  5. 用户点确认:前端把 { confirmed: true, confirmationId, allowForThread? } 写回该工具 part 的 result,发起下一轮请求。
  6. 运行时代执行:新一轮请求进来,运行时扫描消息找到「confirmed 且没有 executionOutcome」的卡,原子消费票据(update … where status='pending' returning),再次校验白名单与注册表后由运行时亲自执行——客户端传的 toolName / input 一律不信。
  7. 结果写回:executionOutcome 被 patch 回那条工具消息并记为 step 0,模型看到真实结果后才继续叙述,杜绝「假装已删除」。

会话 allowlist confirmation-allowlist.ts

「每次都点确认」很烦,但「一次授权全会话」很危险,于是折中:允许放行的只有非破坏性的那一档

存放位置thread.danger_allowlist_json,形如 { toolNames[], updatedAt }
24h TTL过期即清空并按未授权处理(DANGER_ALLOWLIST_TTL_MS
永不入册delete_article、delete_document、delete_ai_config、revoke_article_share、revoke_agent_api_key、set_public_qa_enabled——6 个 critical 工具每次都要卡
实际可放行7 个危险工具里只剩 update_ai_config_credentials 能进 allowlist
命中效果request_user_confirmation 同轮直接执行,返回体带 autoApproved: true 与 executionOutcome,不再出卡

失败降级 playbook tool-resilience.ts

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

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

全量落档 thread-logic.ts

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

thread会话 + 上下文摘要与折叠水位 + 危险 allowlist + 操作员记忆快照
run一次请求一条:intentDomains(LLM 改判后回写)、模型配置、COMPLETED / FAILED 与 errorCode
step每次工具调用:名称(子代理带 spawn_*/d1 前缀)、输入(敏感键 [redacted])、输出、errorCode、耗时 ms
message用户消息进门即存;编辑重提时先按 keepCount 截断旧历史再写入
助手消息流结束时剥掉压缩状态 part 与 warning 级步数条、去重意图 part 后落库,并带 usage / 总耗时 / tokens 每秒
embedding折叠区消息的脱敏摘录 + pgvector 向量,唯一键 message_id
响应头X-Petrichor-Assistant-Thread-Id / Run-Id,前端与日志可互相对账

运行时的设计边界(known limits)

  • 规则路由是纯正则启发式——省钱且可测,但改写措辞可能路由不中;这也是为什么低置信度要交给 LLM 复核,而不是把正则堆得更长。
  • 意图 LLM 只影响芯片与 admin 装载:核心四域已常驻,路由判错的代价被结构性地压小了。
  • token 估算是「字符数 ÷ 2」的粗估,只用于触发阈值,不做精确计费。
  • 熔断状态 per-run 隔离:同一会话下一条消息会重新给失败工具机会,这是刻意选择而非遗漏。
  • 危险操作白名单与域/工具形状由 roadmap 契约锁定(chat-first-universal-agent 4.2–4.4),改动需先过 roadmap update。

07 · 全站星图(本次新增)

让 Agent 自己画一张全站概念地图,再拿它检索

全文检索回答「哪篇文章提到了 X」,但回答不了「A 和 B 有什么关系」。 于是加了第二张地图:由抽取 Agent 从已公开分享的文章里归纳出概念、实体、关系, 校验通过后发布成前台 /graph 的可交互点群,同一份载荷也成了助手与前台问答的检索入口。 代码在 src/server/site-graph/

1

取公开文章

复用前台 loadPublicSiteArticles 的可见性判定(已排除撤销 / 过期 / 加密 / 不可索引),不另写一套规则。

2

确定性骨架

root → 知识库分类 → 文章,外加固定的「概念」「标签」两个分类。不依赖模型——模型全挂也有可用图谱。

3

分批抽取

每 5 篇一批送模型,单篇正文截到 2600 字;每批 ≤12 节点 / ≤20 关系。任一批失败只记 warning 并继续。

4

实体对齐

名称与别名建倒排索引:规范化后精确命中即自动合并;仅名称相近的落成待确认候选,交后台人工拍板。

5

校验 → 发布

结构 / 引用 / 成环 / 孤儿 / 属性合法性检查;存在 error 级问题拒绝发布。新节点一律 DRAFT。

6

前台与检索

公开载荷每次读取都按当前公开文章集重新过滤,供 /graph 点群与 search_knowledge_graph 共用。

抽取 Agent extract-agent.ts

批大小 5SITE_GRAPH_BATCH_SIZE;再大容易超上下文并降低抽取质量
2600 字单篇送模型的正文上限,超出截断并标注 [内容已截断]
回喂已知实体把注册表里权重最高的 60 条实体清单写进提示词,要求「涉及就复用它的 nodeKey」,从源头减少同义分裂
只收两类节点模型只能产 concept / entity;回写文章/分类节点一律忽略(骨架已生成)
关系两端必须已知fromKey/toKey 不在本批文章键或本批新节点里 → 直接丢弃并计一条 warning,这是拦幻觉的第一道闸
跨批累积上一批抽出的实体立刻成为下一批的对齐基准;跨次运行则从库里 loadEntityRegistryEntries 继承

实体注册表 entity-registry.ts

解决「同一事物在不同文章里被起了不同名字」。分两档:精确档自动合并,模糊档只出候选。

归一化NFKC 折全角 → 小写 → 去掉全部非字母数字。于是 Vector Search / vector-search / 全角写法三者等价
精确命中键 / 名称 / 任一别名命中倒排索引 → 直接复用已有规范键,原名降级成别名
相似度二元组 Dice 系数;短串(≤6)另算字符级 Dice × 0.9 取大者,压制字序颠倒误判
候选阈值相似度 ≥ 0.62,或互为子串且较短一方 ≥ 3 字(避免「AI」命中一切)
候选上限单次运行最多 100 条候选,落 merge_candidate 表等人工确认,绝不自动合并

不引图数据库 graph-query.ts

层级走邻接表,跨树关系单独一张表,遍历交给 PostgreSQL 递归 CTE:

层级site_graph_node.parent_id 自引用;structure 连线由它派生,不入关系表
子树with recursive 向下展开,depth 上界同时是递归终止条件——人工改出环也不会无限递归
N 跳邻域在关系表上无向展开,hops 1–3;递归项用 union 去重
祖先链沿 parent_id 上溯,供后台面包屑
SQLite 兜底本地 drizzle sqlite driver 无 execute(),每个查询都配了等价内存遍历(图上限 1200 节点,成本可忽略)

校验与可见性 validate.ts · payload.ts

error 级重复键 / 缺根 / 空名 / 外链 route / 断父 / 文章缺 articleId / 父子成环 / 断边 / 自环 / 空关系名 —— 有一条就拒绝发布
warning 级超 1200 节点或 2400 关系、层级超 6、孤立节点、文章已不公开(发布时自动归档,不再卡发布)
评分100 - error×20 - warning×5,issues 截 200 条
每次读都过滤「当时可公开」≠「现在仍可公开」:前台载荷按当前公开文章集重新过滤,标题与链接也用最新值覆盖
摘掉悬空概念从文章节点出发沿非结构边 BFS,捞不到的概念/实体/标签一并摘掉,再清掉空分类——用度数判断会永远为真,因为所有概念都挂在「概念」分类下
缓存cachePublicContent("siteGraph"),TTL 1 天;生成 / 发布 / 下线 / 合并 / 清空都主动失效

图谱增强检索 qa-retrieval.ts

助手侧新工具 search_knowledge_graph,前台问答共用同一函数、同一渲染:

入口打分名称精确 100 / 别名精确 95 / 名称互含 60×长度比+20 / 别名互含 55 / 属性命中 40 / 摘要命中 30
切词再打分中文整句几乎不可能等于概念名,所以按标点切成候选词逐个打分,取每节点最高分
骨架不当入口root / section 命中没有信息量,直接跳过
扩散限跳 BFS,maxHops 默认 2(1–3),命中入口 ≤5(1–10),节点上限 60、链路上限 12
结构边不扩散否则「同属一个分类」会被当成语义关联
链路优先通文章先回溯通向 article 的路径(可引用、可继续深挖),再补概念链路
hop 0 的文章问句直接命中文章标题时不产生链路,单独收进 articles,避免漏掉最相关那篇
空结果返回 emptyMessage 明确指引改用全文检索,而不是静默返回空数组

两处细节,都是踩过的坑

只读入口拆分助手每次请求都在这条路径上,原先经 service.ts 会连带加载抽取 agent 与 AI generation 链——于是把只读入口单独拆出 public-graph.ts
不重查库直接消费缓存后的公开载荷:可见性规则只实现一次,重查一遍等于把规则再写一遍,极易漏
星图不能替代全文检索星图只覆盖已公开分享的文章,查不到私有知识库。系统提示词里写死:图谱未命中或涉及私有内容时,一律回到 search_knowledge
前台渲染d3-force-3d 力导(固定 2 维)+ Canvas 2D + HTML 标签层;助手卡片里的迷你点群复用同一 runtime,子图无结构边时回退用关系边做布局力
人工优先locked 的节点/关系 Agent 重跑时跳过;source=MANUAL 的数据不会被清理
归档而非删除文章下架 → 节点归档;重新公开时下一次生成会把它放回草稿

08 · 操作员记忆体系

只给操作员的三层记忆,其余用户完全无感

唯一门闩 isAssistantOperator(当前 = 超级管理员,收口在 operator-gate.ts,本栈禁止散落 isSuperAdmin)。非操作员:系统提示里没有记忆段、工具列表里没有 memory 工具,写路径直接 403 assistant_operator_only——三层记忆对普通用户零暴露。

🔒 仅操作员可见的工具
1

常驻短文记忆 operator-memory.ts

petrichor_assistant_operator_profile · 按 user_id 一行

仿 Hermes 的 USER.md + MEMORY.md:两块 Markdown 短文,每轮拼在系统提示最前,标题锁死为「操作员常驻记忆(本线程冻结快照)」。工具 memory_manage 增删改,只写持久 profile

user_profile_md类 USER.md 的用户画像,≤ 1375 字
agent_notes_md类 MEMORY.md 的约定与笔记,≤ 2200 字
合计 ≤ 3575超上限拒写 memory_limit_exceeded,库内容不变
add / replace / removereplace、remove 靠子串命中;未命中 → invalid_patch
2

线程冻结快照 thread.operator_memory_snapshot_json

{ userProfileMd, agentNotesMd, frozenAt }

关键设计:记忆写入只影响新线程。新线程首轮把当时 profile 固化成本线程快照,之后只读;同线程内 memory_manage 改的是持久 profile,本线程快照纹丝不动。幂等:已有快照不再回写覆盖。

持久 profile

operator_profile 表

memory_manage 实时改这里

线程 A · 快照

frozenAt = T1

首轮固化即冻结,后续再问仍读旧内容

线程 B · 快照

frozenAt = T2

新开线程首轮才看到 T1→T2 的改动

3

跨线程情景检索 operator-episodic.ts

petrichor_assistant_message_embedding · 折叠区脱敏摘录

想回忆「上次那件事怎么做的」时,模型主动调 search_operator_history 跨自己所有线程检索——不是把整段历史塞进上下文,而是按需捞片段。数据源正是第 03 节折叠时脱敏落库的那批向量。

keywordPostgres 全文 to_tsvector / plainto_tsquery(simple 配置)+ ts_rank 排序
semanticpgvector 余弦,需用户配了嵌入模型;没配静默跳过
both(默认)两路并行 + 按 threadId:messageId 去重合并(语义结果优先),semantic 阈值 ≥ 0.25
excludeCurrentThread默认排除当前线程;limit ≤ 20;SQLite 环境回退 ilike
二次脱敏取出来的摘录再过一遍 sanitizeRecallExcerpt,历史脏数据也不会漏

记忆的设计边界(known limits)

  • 记忆按 user_id 隔离,多个超管互不共享;门闩当前等价于 isSuperAdmin
  • 写入永不改本线程冻结快照——「同一线程里看不到刚写的记忆」是刻意行为,不是 bug。
  • 情景检索的 semantic 分支依赖用户配了嵌入模型;没配则只剩关键词 FTS,SQLite 环境进一步退化为 ilike。
  • 常驻短文有硬字数上限(1375 / 2200 / 合计 3575),塞不下就得先删旧的。
  • 原第 4 层「可写 Skills 与手动进化」已整体移除:三节面板上线以来零数据,连同后端、API 路由与两张表一并删除。operator_skill 表与读路径保留但只读不写,行为等价于「只有内置 skill」,便于日后恢复。