Working Memory
【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking
Session Title
简短独特的 5-10 词标题,信息密集
Current State
当前工作状态、待完成任务、下一步
Task & Goals
用户目标、关键设计决策、解释性上下文
Key Facts & Decisions
重要结论、技术选择及理由、用户偏好与约束
Files & Context
重要文件 / 函数 / 模块及路径
Errors & Corrections
遇到的错误及修复、用户纠正、失败方案
Open Issues
未解决问题、阻塞项、后续风险
模板在 [session.py](https://link.gitcode.com/i/a654d680b56087e9f23a2e21dc0456c6) 中由 `WM_SEVEN_SECTIONS` 常量定义,服务端解析与合并均围绕该常量遍历。段与段的「数据性格」不同,决定了 Guard 策略不同: - **锚定型**(Session Title):会话身份标识,不应随意变更; - **易变型**(Current State、Task & Goals):每轮反映当前状态,LLM 可自由 UPDATE; - **累积型**(Key Facts & Decisions):重要结论不断积累,丢失代价高; - **引用型**(Files & Context):文件路径一旦提及不应消失; - **只增型**(Errors & Corrections):错误记录只增不删; - **跟踪型**(Open Issues):未解决项不应被静默丢弃。 从源码看,预算约束还落在 prompt 指引层面:每段上限约 2000 tokens、总 WM 上限约 12000 tokens;服务端定义 `_WM_SECTION_BULLET_THRESHOLD = 25`、`_WM_SECTION_TOKEN_THRESHOLD = 1500`([session.py](https://link.gitcode.com/i/a654d680b56087e9f23a2e21dc0456c6#L4737-L4738)),当单段超过 25 个 bullet 或 1500 tokens 时触发 consolidation 提醒。 --- ## 四、增量更新协议:tool_call + JSON Schema WM 更新通过 **tool_call(function calling)+ JSON schema** 实现:LLM 调用 `update_working_memory` 工具,以结构化 JSON 提交对 7 个段的逐段操作。JSON schema 的强约束保证漏段、多段、格式错误在 schema 层直接拦截。 ### 4.1 tool schema 定义 ```python WM_SEVEN_SECTIONS = [ "Session Title", "Current State", "Task & Goals", "Key Facts & Decisions", "Files & Context", "Errors & Corrections", "Open Issues", ] WM_UPDATE_TOOL = { "type": "function", "function": { "name": "update_working_memory", "parameters": { "type": "object", "required": ["sections"], "additionalProperties": False, "properties": { "sections": { "type": "object", "required": list(WM_SEVEN_SECTIONS), # 7 段全部必填 "additionalProperties": False, "properties": {name: _WM_SECTION_OP_SCHEMA for name in WM_SEVEN_SECTIONS}, } }, }, }, }每段的操作(_WM_SECTION_OP_SCHEMA)用oneOf约束为三种形状之一:
{"op": "KEEP"}— 原样保留{"op": "UPDATE", "content": "..."}— 全段替换{"op": "APPEND", "items": ["...", "..."]}— 追加条目
实现细节上,op字段使用"type": "string", "enum": ["KEEP"]形式,以兼容更多 JSON Schema 版本;additionalProperties: false+required把 LLM 输出严格钉在 schema 里。7 段全部必填的设计,强制 LLM 对每个段落都显式表态,避免「某段被遗忘而隐式丢失」。
4.2 段级合并
服务端_merge_wm_sections(old_wm, ops)按WM_SEVEN_SECTIONS常量遍历 7 段执行合并:
- KEEP→ 原样复制旧内容;
- UPDATE→ 用 LLM 提供的 content 替换(先经过该段的 guard 校验);
- APPEND→ 旧内容 + LLM 提供的 items(渲染为
- item); - 漏段 / 未知 op→ 兜底 KEEP。
配套的_parse_wm_sections()按##标题切分 Markdown 为{header: body}字典。关键实现位于 session.py 的_parse_wm_sections()(约 L4715)与_merge_wm_sections()(约 L5367)。
五、服务端 Guards:信息保留的系统级兜底
Guards 是服务端在合并 LLM 提交的操作时按段执行的语义校验函数:即使 LLM 说 UPDATE,服务端也根据段的特性决定是否接受。7 个段的保护策略如下:
| 段 | 数据特点 | Guard | 规则 |
|---|---|---|---|
| Session Title | 锚定型 | _wm_enforce_title_stability | UPDATE 与旧 title 的 meaningful-word overlap < 1 → 回退 KEEP |
| Current State | 易变型 | 无 | LLM 可自由 UPDATE |
| Task & Goals | 易变型 | 无 | LLM 可自由 UPDATE |
| Key Facts & Decisions | 累积型 | _wm_enforce_key_facts_consolidation | 双阈值验证:bullet count ≥ 旧值 15% 且 lexical anchor coverage ≥ 70%;被拒时提取新 items 做 APPEND |
| Files & Context | 引用型 | _wm_enforce_files_no_regression | UPDATE 丢失旧路径 → KEEP + APPEND 新路径 |
| Errors & Corrections | 只增型 | _wm_enforce_append_only | UPDATE 降级为 APPEND,去重后只追加新条目 |
| Open Issues | 跟踪型 | _wm_enforce_open_issues_resolved | silently drop 的 item → 加[restored]标签恢复 |
从源码看,这些 guard 的工程实现相当细致(session.py):
- Errors 的 append-only 降级:
_wm_enforce_append_only()将 UPDATE 的 content 解析为 bullet items,过滤掉旧内容中已存在的条目后,把「真正新增的条目」以 APPEND 形式重新发射,既不让 LLM 重写的内容丢失,也绝不丢弃旧 body 的任何内容;若没有新增条目则整体回退为 KEEP。 - Key Facts 的受控合并:
_wm_enforce_key_facts_consolidation()采用双层验证。第一层拒绝明显缩水的 UPDATE(bullet 数 < 旧值 15%);第二层要求至少 70% 的lexical anchor覆盖——_extract_lexical_anchors()会从文本中提取日期(如2024-03-15)、数字短语(如3 months、$200)、决策标记词(because / decided / chose / agreed / resolved)以及专有名词(大写 token)作为事实锚点(session.py)。被拒的 UPDATE 会通过_salvage_new_items_from_rejected_update()抢救出真正的新条目做 APPEND,当前轮次的事实不会静默丢失。 - Key Facts 的反膨胀(anti-bloat)机制:当 Key Facts 已超限(bullet > 25 或 token > 1500)时 APPEND 被节流——1~2 倍阈值区间只接受去重后的新条目且上限 5 条(
_WM_OVERSIZED_APPEND_CAP = 5);超过 2 倍阈值(紧急状态)则拒绝一切普通新增,仅插入一条幂等的[⚠ CONSOLIDATION REQUIRED: ...]哨兵,且哨兵已存在时后续轮次不再追加任何内容,形成硬停止(session.py)。 - Files 的路径回归检测:
_WM_PATH_LIKE_RE用宽松的 path-like 正则识别旧 Files & Context 中出现过的文件路径(覆盖py/ts/tsx/js/jsx/md/yaml/yml/json/sh/ps1/cmd/bat/toml/ini/cfg/rs/go扩展名及a/b/c型路径),UPDATE 一旦丢失旧路径即回退 KEEP 并追加新路径。 - Title 的停用词机制:
_WM_TITLE_STOPWORDS过滤the/a/an/and/session/working/memory等无信息量词汇后,再计算新旧 title 的重叠度判断是否「换了个身份」。
另外,更新 prompt 中还会注入动态的section size warnings:_build_wm_section_reminders()扫描当前 overview,统计每段 bullet 数与 token 估算,超过阈值时生成<section_size_warnings>XML 块,明确告知 LLM 哪些段必须用 UPDATE 做合并压缩、并给出目标(≤ 25 bullets、≤ 1500 tokens),从而把「何时必须压缩」的信号从服务端传到模型(session.py)。
这些 guard 均有配套单元测试,集中在 tests/unit/session/test_wm_v2_guards.py(共 107 用例覆盖 5 个 guard + growth + 通用 schema),另有test_working_memory_growth.py、test_working_memory_v2.py等测试文件补充验证。
六、滑动窗口与 pending_tokens
SessionMeta维护pending_tokens: int与keep_recent_count: int两个字段,持久化到.meta.json。其核心语义(session.py):
pending_tokens是落在最近保留窗口之外、将在下次 commit 被归档的消息的累计 estimated_tokens;keep_recent_count是插件最近一次通过 commit API body 传入的值,被记住后,后续add_message调用可在进程重启后依然一致地维护pending_tokens。
运行机制:
add_message时:新消息进入保留窗口尾部,窗口头部被挤出的消息 token 累加到pending_tokens;commit时pending_tokens归零;GET /sessions/{id}直接读 meta,O(1) 获得 pending_tokens。
服务端有防御性 clamp:pending_tokens与keep_recent_count在加载与写入时都执行max(0, ...)(session.py)。CommitRequest.keep_recent_count在 router 层还有ge=0, le=10_000的数值约束,见 routers/sessions.py 的CommitRequest定义。此外从源码看,commit 之后服务端还会通过_rebuild_pending_tokens()依据当前消息列表重算pending_tokens(session.py),保证跨重启、跨异常路径下的账目一致。
关键实现位置:session.py的SessionMeta/add_message()、routers/sessions.py的CommitRequest。
七、保留最近消息:keep_recent_count
commit 归档时并不全量清空消息,而是保留最近 N 条以维持上下文连贯:
- 参数
keep_recent_count由插件在 commit API body 中传入; - afterTurn 路径默认 10,compact 路径硬编码 0;
- OV 存储模型保证
tool_use/tool_result配对完整性(ToolPart 自包含),因此归档与保留都不会拆散一次工具调用与其结果。
该值会被持久化到SessionMeta.keep_recent_count,确保跨进程重启后滑动窗口的边界仍与插件侧预期一致。关键实现:session.py的commit_async(keep_recent_count)、routers/sessions.py的CommitRequest,以及插件侧的context-engine.ts、client.ts。
八、核心流程
8.1 afterTurn 流程(每轮对话后自动归档)
插件端逻辑不变,commit 的耗时工作在服务端完成:
[插件] afterTurn ├── extractNewTurnMessages → 提取新消息 ├── addSessionMessage → 逐条 POST /sessions/{id}/messages │ 服务端: append msg + 滑动窗口更新 pending_tokens + save meta ├── GET /sessions/{id} → 返回 pending_tokens(O(1)) └── pending_tokens >= tokenBudget * commitTokenThresholdRatio? │ YES → commitSession(wait=false, keepRecentCount=cfg.commitKeepRecentCount) │ [服务端 commit_async] │ ├── Phase 1(同步,不阻塞返回) │ ├── split_idx = total - keep_recent_count │ ├── 归档 messages[:split_idx] → archive_NNN/ │ ├── 保留 messages[split_idx:] │ └── pending_tokens = 0, 更新 meta │ └── Phase 2(asyncio.create_task 后台执行,包在 request_wait_tracker.register_request / wait_for_request / cleanup 包络内,确保所有下游 enqueue 都被等待) ├── 读旧 WM: _get_latest_completed_archive_overview() ├── 有旧 WM? │ YES → ov_wm_v2_update prompt + tool_call │ → guards 检查每段决策 │ → _merge_wm_sections 段级合并 │ NO → ov_wm_v2 prompt 全量创建 ├── 写入 archive_NNN/.overview.md + .abstract.md + .meta.json ├── 提取 long-term memory(SessionCompressorV3,需 archive_uri 才能写 memory_diff.json) ├── 等待 embedding / semantic 队列排空(wait_for_request) └── 写入 .done(最后写,标志该 archive 全部状态终结)Phase 2 的关键细节(均有源码对应):
- 格式检测:读取旧 overview 后,先检查是否包含 WM 7 段 header(
any(f"## {s}" in overview for s in WM_SEVEN_SECTIONS))。若是 legacy 格式,走创建路径而非 tool_call 更新,保证平滑升级。 - Section reminders:更新路径中
_build_wm_section_reminders()从旧 WM 提取每段当前状态摘要与尺寸告警,注入到 update prompt 的wm_section_reminders变量。 - 完整回退链:tool_call 缺失 →
_fallback_generate_wm_creation()重跑(传入旧 WM 作为上下文);JSON parse 失败 → 正则 recovery(_wm_recover_ops_from_raw()针对 VLM 后端包装非 JSON 参数、未转义字符、弯引号、截断 JSON 等场景做逐段恢复)→ 段级 guard 兜底 KEEP;VLM 不可用 → 占位 summary。 - Phase 2 队列等待:
register_request+wait_for_request(timeout=_PHASE2_QUEUE_WAIT_TIMEOUT_SECONDS=1800s)是必需的——否则下游compressor/memory_updater通过register_*_root注册的 embedding / semantic 队列无人 await,会让tracker.complete()与.done在向量化 / 语义入库之前就触发,导致调用方看到 commit 完成但 memory 不可检索。
Phase 2 主循环对应 session.py 的_run_memory_extraction()。
8.2 compact 流程(主动上下文压缩)
[插件] compact └── commitSession(wait=true, keepRecentCount=0) ├── Phase 1: 全部消息 → archive, messages.clear() ├── Phase 2: 读旧 WM → 创建/更新 → 写入 └── 返回 → getSessionContext → 回读最新 WM与 afterTurn 的差异在于:wait=true同步等待完成;keepRecentCount=0表示 Phase 1 全量归档并清空 live messages,实现彻底压缩。
8.3 assemble 流程(上下文组装)
assemble 将上下文组织为 instruction / archive / session 三分区:
┌──────────── System Prompt ────────────────────┐ │ systemPromptAddition(语义示意,非逐字): │ │ 1. [Session History Summary] 是压缩摘要 │ │ 2. Active messages 是最新未压缩上下文 │ │ 3. 二者冲突时优先 active messages │ │ 4. 缺细节时询问用户,不要猜 │ │ + 原始 system prompt │ └────────────────────────────────────────────────┘ ┌──── Layer 1: Archive Memory (≤8K tokens) ─────┐ │ [user] [Session History Summary] │ │ # Working Memory │ │ ## Session Title │ │ ## Current State │ │ ## Task & Goals │ │ ## Key Facts & Decisions │ │ ## Files & Context │ │ ## Errors & Corrections │ │ ## Open Issues │ └────────────────────────────────────────────────┘ ┌──── Layer 2: Session Context ─────────────────┐ │ server 侧合并后的 ctx.messages: │ │ - 未完成 archive 的 pending messages │ │ - 当前 live session messages │ └────────────────────────────────────────────────┘ ┌──── Layer 3: Reserved (≥20K tokens) ──────────┐ │ LLM 回复空间 │ └────────────────────────────────────────────────┘【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考