DSH harness 中的上下文管理探秘

Agent 记忆管理:上下文管理 + 长期记忆

覆盖:上下文管理完整清单(传统 vs DSH)、决策依据、关键概念(投影/KV缓存/最近尾巴)、mem0、分层架构、路由(agent/模型)、面试追问。 图用 Mermaid;DSH 结论来自其源码包(dsh-session / dsh-token-meter / dsh-compaction-basic / dsh-compaction-tool-result-pruner / dsh-system-prompt / dsh-output-retention / dsh-subagent / dsh-skill)。


0. 记忆管理的两层结构

记忆管理分两层,按"数据存在哪里"划分:

数据位置 生命周期 代表技术
短期 / 工作记忆 模型上下文窗口内 单次会话 token 计量、压缩、裁剪、滑窗、KV 缓存
长期记忆 窗口外的外部存储 跨会话 提取 + 语义检索 + 更新(mem0、LangGraph Store)

1. 上下文管理:8 类清单

每类按【要解决的问题 → 传统做法(LangChain/LangGraph)→ DSH 做法】展开。

1.1 上下文窗口(容量)

  • 问题:模型单次请求的 token 数有上限(如 128k),所有缩减策略都要基于这个容量做预算。
  • 传统:直接往 messages 数组塞,塞爆后收到 provider 报错再手工删减重试。
  • DSH :容量不写死,由 LLM adapter 在路由时声明 ctx.llm.resolveModelInfo().context;触发压缩的阈值(thresholdRatio 0.8)和压缩后保留的尾巴(retainRatio 0.16)是两个独立参数。

1.2 Token 计量

  • 问题:需要知道"当前请求多少 token、下一次请求会多少 token"来决定是否缩减。
  • 传统 : LangChain:get_num_tokens() 用 tiktoken 同步全量计数;ConversationTokenBufferMemory 每次加消息重新计算一遍。 LangGraph:无内建 meter,trim_messages 需自传 token_counter
  • DSHdsh-token-meter):
    • 固定启发式 4 字符/token + 结构开销,只做参考值,不做计费/门控。
    • 增量折叠(replay-aware):折叠会话事件日志维护累加状态,O(surface),不每次全量重数。
    • 复用 provider usage:最新请求的"规范信封"与当前一致时,直接用 provider 报的 usage(分 input / cache-read / cache-write / output 四桶),否则退回启发式。
    • projectedTokens:估算下一请求的 token 数(压缩是旁路调用不记 usage,纯 provider 报数会滞后,用"锚点 + surface 增减"即时反映)。
    • 三种口径:pressureTokens(provider 报的 prompt 大小)、projectedTokens(下一请求预估)、contextBreakdown(启发式拆成 system/tools/message)。
flowchart LR EV[&#34;会话事件日志&#34;] --> FOLD[&#34;surface-fold 增量折叠<br/>O(surface)&#34;] FOLD --> H[&#34;启发式 4字符/token + 结构开销&#34;] USAGE[&#34;provider 报的 usage<br/>input/cache-read/cache-write/output&#34;] --> REUSE{&#34;最新信封与当前一致?&#34;} REUSE -- &#34;是&#34; --> H2[&#34;复用 provider 数值为锚点&#34;] REUSE -- &#34;否&#34; --> H H2 --> PROJ[&#34;projectedTokens = 锚点 + surface 增减&#34;] PROJ --> OUT[&#34;contextBreakdown<br/>system/tools/message 拆解&#34;]

1.3 消息历史存储与派生("删除"语义)+ 投影

  • 问题:对话历史怎么存、怎么变成每次发给模型的 messages、以及"删掉某些历史"在数据层怎么表达。
  • 传统
    • LangChain:内存 messages 列表;ConversationBufferWindowMemory / TokenBufferMemory 直接从数组 drop 旧消息,真删,删了无法回放。
    • LangGraph:state.messages + checkpointer(MemorySaver/SqliteSaver/PostgresSaver)按 thread 持久化快照;RemoveMessage(id) 从 state 移除。
  • DSHdsh-session):
    • 事件溯源Session 是 append-only 事件日志(唯一事实源),模型消息从日志派生(deriveMessages() 投影成 surface)。
    • 删除 = replace :追加一条带 surfaceOp: {op:'replace', start, end} 的新事件,旧事件仍在日志,只是不再投影。带 sourceEventSeqs 溯源"这条替换覆盖了哪些原始 seq"。
    • 换来:可审计、可确定性回放、可 fork、崩溃可修复。
flowchart TB subgraph LC[&#34;LangChain(旧)&#34;] A1[&#34;内存 messages 数组&#34;] --> A2[&#34;直接增删,删了无法回放&#34;] end subgraph LG[&#34;LangGraph&#34;] B1[&#34;state.messages&#34;] --> B2[&#34;checkpointer 按 thread 持久化快照&#34;] end subgraph DSH[&#34;DSH&#34;] C1[&#34;Session:append-only 事件日志(唯一事实源)&#34;] --> C2[&#34;deriveMessages() 派生&#34;] C2 --> C3[&#34;surface:模型可见消息&#34;] C3 --> C4[&#34;replace 覆盖旧 span,日志永不删&#34;] end

投影(projection)与"原值 ↔ 投影"查找

  • 投影 = 把事件流折叠(fold)成的一个"读模型"视图,可由日志确定性重算。DSH 有两处:dsh-session 的 surface(日志 → messages);dsh-session-projectioninit/apply/view 折叠成 UI 读模型如 tokenUsage、todo、goal)。
  • 查找关系:
    • 投影 → 原值:消息事件的 sourceEventSeqs 记录它引用的原始事件 seq;替换事件的 surfaceOp.start/end 记录被覆盖的 span。
    • 原值 → 投影:append 型消息事件 1:1 映射到自己的 surface 条目;被 replace 覆盖的原始事件已不在 surface 上,靠覆盖它的替换事件(sourceEventSeqs 含该 seq,或 surfaceOp span 覆盖它)反查。
flowchart TB subgraph LOG[&#34;日志(原值)&#34;] E1[&#34;seq=1 user/message&#34;] E2[&#34;seq=2 assistant/message&#34;] E3[&#34;seq=3 tool/result&#34;] E4[&#34;seq=4 user/message<br/>surfaceOp: replace 1-3<br/>sourceEventSeqs: [1,2,3]&#34;] end subgraph SURF[&#34;surface(投影)&#34;] S1[&#34;summary 消息(由 E4 投影)&#34;] end E4 -->|&#34;正向:sourceEventSeqs=[1,2,3]&#34;| S1 E1 -.->|&#34;反向:被 E4 的 span 覆盖&#34;| E4

1.4 压缩总结(compaction)

  • 问题:把一大段历史换成一小段摘要,尽量不丢关键信息。
  • 传统
    • LangChain:ConversationSummaryMemory 每轮把"旧摘要 + 新消息"再浓缩;SummaryBufferMemory 近 N 条保留原文 + 更旧进摘要。自由文本,不保证变短,不处理工具调用。
    • LangGraph:summarize_conversation 预模型钩子,自由文本摘要。
  • DSHdsh-compaction-basic + dsh-compaction 服务定义):
    • 触发agent/pre-step 压力 ≥ 阈值;或 agent/request-error 收到溢出;或手动 /compact
    • 保留 :只压最老一段,保留最近 retainRatio/retainTokens;切分边界落在工具调用/结果配对完整处。
    • 结构化模板:8 章节(Primary Request and Intent / Key Technical Concepts / Files and Code / Errors and Fixes / Pending Jobs / Current Work / Next Step / Critical Context)。
    • 可合并:二次压缩时合并旧 checkpoint(保留仍真、删过时、合并新信息),不叠两层摘要。
    • 收敛校验 :必须短于原文,否则拒绝;compactionRetries 次仍不达标抛错。
    • KV 缓存复用:总结请求把"系统提示 + 工具 + 被压缩区间"逐字节重放为前缀,复用 provider 缓存前缀。
    • 只存文本:丢弃 reasoning 和 tool_call(防泄露私有推理、防孤儿工具调用)。
    • 事务性compaction/start → summary → 替换 → end,中途崩溃留下可检测的孤儿锁。
sequenceDiagram participant Agent as agent-loop participant C as compaction-basic participant LLM as llm.stream participant S as Session 日志 Agent->>C: pre-step 检查压力 C->>S: append compaction/start(加锁) C->>LLM: 逐字节重放系统提示+工具+被压缩区间作前缀 LLM-->>C: 结构化 8 章节摘要 C->>C: 校验:必须短于原文,否则拒绝+重试 C->>S: append compaction/summary C->>S: append user/message(replace) C->>S: append compaction/end(解锁)

1.5 裁剪 / 截断(truncation / pruning)

  • 问题:单个消息(尤其工具结果)太大,截到预算内。不调 LLM,纯字符串操作。
  • 传统trim_messages(max_tokens, strategy='last'/'first'),通用、按 token、不区分消息类型、无精确省略元数据。
  • DSH
    • dsh-compaction-tool-result-pruner:对超预算 tool/result 做 head/middle/tail(默认 head 4096 + tail 1024 字符,超 8192 触发),写 replace 事件,原始结果保留在日志。
    • dsh-output-retentionItemRetainer(保前 N 条目)、TextRetainer(head/tail/headTail,按 UTF-8 边界),返回精确省略计数,让工具把"中间省略 N 行"告诉模型。
    • 裁剪是语法级(保头保尾,不判断中间哪行重要);字符预算 ≠ token 预算,最终由 token-meter 判定。
flowchart LR T[&#34;超预算 tool/result&#34;] --> P[&#34;dsh-compaction-tool-result-pruner&#34;] P --> R[&#34;保留 head 4096 + tail 1024 字符<br/>中间 [... tool result middle pruned ...]&#34;] R --> W[&#34;写入 replace 事件,原始结果保留在日志&#34;] T2[&#34;超预算文本/条目&#34;] --> O[&#34;dsh-output-retention&#34;] O --> O2[&#34;ItemRetainer 保前 N 条目<br/>TextRetainer head/tail/headTail&#34;] O2 --> O3[&#34;返回精确省略计数告诉模型&#34;]

1.6 滑窗 / 最近尾巴(保留最新 N 条)

  • 问题:只保留最近的部分历史,丢弃更旧的。
  • 传统ConversationBufferWindowMemory(k=N) 只留最近 N 次交互,直接丢更旧的
  • DSHretainRatio/retainTokens 保留最近尾巴,但更旧的不是丢,而是压缩成摘要(组合式)。
  • 最近尾巴 = 压缩时保持原文不动的那段最近消息。必须保留原文,因为最近交换/进行中的工作/未闭合工具调用需要精确状态。
  • 注意:纯"保留最新 N"按顺序取舍、不按相关性,早期关键约束会被丢掉------这正是需要长期记忆补位的原因。
flowchart LR OLD[&#34;最老 span → 总结成摘要&#34;] -->|replace| SUM[&#34;compacted-summary 摘要&#34;] TAIL[&#34;最近尾巴 → 保持原文&#34;] --> TAIL2[&#34;最近的若干条消息(原文)&#34;]

1.7 溢出恢复

  • 问题:provider 已报"超窗口",不能直接失败,要走恢复路径。
  • 传统:手写 try/except + trim 重试。
  • DSH :adapter 归一化 CONTEXT_WINDOW_EXCEEDEDagent/request-error → 绕过阈值强制缩减(prune + 最大平衡头部缩减)→ dsh-llm-retry 退避重试。
flowchart LR P[&#34;provider 报 context_length_exceeded&#34;] --> N[&#34;adapter 归一化 CONTEXT_WINDOW_EXCEEDED&#34;] N --> R[&#34;agent/request-error 瀑布&#34;] R --> S[&#34;裁剪 → 最大平衡头部缩减&#34;] S --> RT[&#34;llm-retry 授权重试,指数退避+jitter&#34;] RT --> P

1.8 上下文组装 / 注入 / 隔离(做"加法"和"分流")

  • 系统提示词 + 工具 schema 组装dsh-system-promptorder 固定排序 section、{{variable}} 插值、显式 toolOrder;前缀逐字节稳定是为了 KV 缓存命中;卸载一个工具就省掉整套 schema token。
  • 运行时注入dsh-time-context / dsh-tmux-context(变化才注入);dsh-session-reference(跨会话有预算上限的只读快照 + 不可信警告)。
  • 隔离dsh-subagent / fork / workflow(子 agent 独立窗口,回传紧凑结果);dsh-skill(技能指令按需加载,非常驻)。
flowchart TB subgraph ADD[&#34;注入(加法)&#34;] A1[&#34;dsh-system-prompt:section 固定排序 + 变量插值 + toolOrder&#34;] A2[&#34;dsh-time-context / dsh-tmux-context:变化才注入&#34;] A3[&#34;dsh-session-reference:跨会话有预算上限的只读快照&#34;] end subgraph ISO[&#34;隔离(分流)&#34;] B1[&#34;dsh-subagent / fork / workflow:子 agent 独立窗口&#34;] B2[&#34;dsh-skill:技能指令按需加载&#34;] end

KV 缓存(跨请求 prompt 前缀缓存)

  • 推理层 KV cache:生成时缓存已算 token 的 K/V 向量,避免每步重算注意力。
  • 跨请求 prompt cache(上下文管理谈的是这个):provider 缓存 prompt 前缀的 K/V;新请求前缀逐字节一致就复用、只算新后缀,命中部分按 cache_read 计费(更便宜)。
  • 存/查:服务端按前缀哈希存、带 TTL;客户端不能直接查缓存,只能通过 usage 字段(cache_read_tokens/cache_write_tokens)观察命中。
  • 客户端如何配合命中:静态内容放最前且排序固定;历史只 append 不中间插入;压缩时逐字节重放前缀。
flowchart LR A[&#34;静态内容放最前 system prompt + 工具 schema&#34;] --> B[&#34;排序固定、逐字节稳定&#34;] B --> C[&#34;历史只 append,不在中间插入/修改&#34;] C --> D[&#34;压缩时逐字节重放前缀&#34;] D --> E[&#34;新内容只出现在末尾&#34;]

2. 决策依据:什么时候做什么

唯一度量源ctx.tokenMeter.measure() 的压力值 vs 路由模型 resolveModelInfo().context 容量。

三个触发点 × 动作阶梯

flowchart TB P[&#34;pre-step 发请求前&#34;] --> M[&#34;measure() 测压力&#34;] M --> T{&#34;压力 ≥ window × thresholdRatio?&#34;} T -- &#34;否&#34; --> GO[&#34;正常发请求&#34;] T -- &#34;是&#34; --> PR[&#34;① pruner 裁剪超预算 tool result&#34;] PR --> M2[&#34;② 重测&#34;] M2 --> T2{&#34;仍超?&#34;} T2 -- &#34;否&#34; --> GO T2 -- &#34;是&#34; --> SUM[&#34;③ LLM 压缩,压最老 span 留尾巴&#34;] SUM --> M3[&#34;④ 重测,超则 compactionRetries 重试&#34;] M3 --> GO GO --> ERR{&#34;provider 报溢出?&#34;} ERR -- &#34;是&#34; --> REC[&#34;绕过阈值:prune + 最大平衡头部缩减 + 重试&#34;] REC --> GO

决策表

触发点 条件 动作 参数
agent/pre-step 压力 ≥ window × thresholdRatio 裁剪 → 重测 → 压缩 → 收敛 thresholdRatio(0.8)、retainRatio(0.16)、compactionRetries(1)
agent/request-error provider 报溢出(归一化 CONTEXT_WINDOW_EXCEEDED 绕过阈值强制缩减 + 重试 maxOverflowRetries(1)
手动 /compact 用户显式命令 低于阈值也压一段 ---

动作阶梯顺序(省成本):先无 LLM 裁剪(零模型调用)→ 重测够了就停 → 不够才调 LLM 压缩(贵)→ 收敛校验 → 溢出走独立强制路径。

注入侧触发总表

处理点 何时触发 依据
system prompt 组装 每步 恒执行
time / tmux 上下文 每步,但状态变化或超 refreshIntervalMs 才注入 变化检测
session-reference 用户 @ 提及会话 显式
skill 加载 任务匹配技能名 按需
subagent / workflow 显式委派/扇出 显式

2.2 每个策略的时机(策略 × 触发钩子 × 条件)

策略 触发时机(生命周期钩子) 触发条件 说明
Token 计量(更新) 每次 surface 变化(append/replace 提交后) 事件驱动,持续维护 增量折叠;查询在 pre-step 用 measure()
裁剪(pruner) 压缩触发后、LLM 压缩前 压力 ≥ 阈值 或 溢出恢复 只在缩减路径执行;低于阈值的正常步骤从不裁剪
压缩(compaction) agent/pre-stepagent/request-error;手动 /compact 压力 ≥ window×0.8;溢出;用户命令 手动可低于阈值
保留尾巴(retain) 压缩内部 随压缩确定边界 不是独立触发,是压缩的参数
收敛重试 压缩内部(压缩后重测) 仍超阈值 compactionRetries
溢出恢复 agent/request-error provider 报 CONTEXT_WINDOW_EXCEEDED 绕过阈值强制缩减 + maxOverflowRetries
重试(llm-retry) agent/request-error 错误码 ∈ EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT 退避 500ms--10s + jitter
组装(system-prompt) 每步(请求派生前) 恒执行 前缀固定排序,保证 KV 命中
注入(time/tmux) agent/pre-step 状态变化 或 超 refreshIntervalMs 变化才注入,避免重复
注入(session-reference) 用户 @ 提及会话时 显式 有字节预算上限
skill 加载 任务匹配技能名时 按需 非常驻
subagent / workflow 显式委派/扇出时 显式 独立窗口
KV 缓存(配合) 持续约束(非触发) 组装/压缩时保持前缀稳定 压缩时重放前缀复用

时序关键点(为什么是这个顺序)

  1. 缩减动作按成本从低到高:先裁剪(无 LLM 调用)→ 重测 → 不够才压缩(调 LLM)
  2. 正常路径只在 pre-step 做一次压力检查;溢出路径在 request-error 强制缩减,两者不重叠。
  3. 计量是"持续维护 + 按需读取":事件驱动更新,决策点才 measure()

2.3 一次请求的生命周期(各策略挂在哪个钩子)

sequenceDiagram participant User as 用户输入 participant Pre as agent/pre-step participant Derive as 请求派生 participant LLM as llm.stream participant Err as agent/request-error participant Log as Session 日志 User->>Pre: 进入 inbox Pre->>Pre: 组装 system-prompt(每步) Pre->>Pre: time/tmux 注入(变化才注入) Pre->>Pre: measure() 压力检查 Pre->>Pre: 超阈值则 pruner 裁剪 → 重测 → 压缩 Pre->>Derive: 请求派生(deriveMessages + header) Derive->>LLM: 发送请求 LLM-->>Log: 成功:记录 assistant + usage(token-meter 增量) LLM-->>Err: 失败 Err->>Err: 溢出则溢出恢复;其他则 llm-retry 退避重试 Err->>Derive: 重试(同一历史)

3. mem0

3.1 定义

mem0 是一个开源的记忆层(memory layer) ,给 LLM 应用/agent 提供独立于上下文窗口的长期记忆。论文:Mem0 (arXiv 2504.19413)

两阶段流水线

  1. 提取:一个 LLM 从对话消息里抽取"值得记的事实/偏好/决策",输出结构化 memory 条目(JSON)。
  2. 更新 + 存储 :对每条候选记忆,LLM 拿它和已有相关记忆比较,判定 ADD / UPDATE / DELETE / NOOP,写入向量库。
  3. 检索search(query, user_id, agent_id, run_id) → query 向量化 → 相似度检索 → 按作用域过滤 → top-k。

存储组件:向量库(Qdrant/Chroma/pgvector/FAISS)、历史库(SQLite/Postgres,记录 add/update/delete 历史)、图库(Neo4j,Graph Memory)、LLM(提取+更新)、Embedder(向量化)。

flowchart LR M[&#34;对话消息&#34;] --> E[&#34;LLM 提取事实/偏好/决策&#34;] E --> C[&#34;候选记忆条目 JSON&#34;] C --> U[&#34;LLM 判定 ADD/UPDATE/DELETE/NOOP&#34;] U --> V[(&#34;向量库 Qdrant/Chroma/pgvector&#34;)] U --> H[(&#34;历史库 SQLite/Postgres&#34;)] U --> G[(&#34;图库 Neo4j / Graph Memory&#34;)] Q[&#34;查询 query + user_id/agent_id&#34;] --> EM[&#34;embedder 向量化&#34;] EM --> SR[&#34;相似度检索 + scope 过滤&#34;] V --> SR SR --> K[&#34;top-k 相关记忆&#34;] K --> I[&#34;作为文本注入上下文&#34;]

作用域与类型官方 memory-types 文档):

维度 取值
作用域 user(跨会话用户偏好/约束)、agent(agent 策略/行为)、sessionrun_id
类型 episodic(事件)、semantic(事实/偏好)、procedural(怎么做 X 的步骤)、graph(实体-关系)

3.2 与上下文管理(第 1 章)的逐项区别

维度 上下文管理(窗口内方案) mem0
存储位置 请求体/进程内存里的 messages 外部向量库 + 历史库(+图库)
生命周期 单次会话 跨会话持久化
数据形态 原始消息文本 / 摘要文本 结构化 memory 条目(向量 + JSON)
写入方式 append 然后 replace LLM 判定 add / update / delete / noop
读取方式 按顺序全部送进请求 按 query 语义相似度检索 top-k 再注入
取舍依据 时间顺序 / 位置 / 结构边界 语义相关性(+图关系遍历)
一致性维护 无(原文或摘要,不合并) 有:去重、纠正、合并(consolidation)
写入是否调 LLM 压缩调;裁剪/滑窗不调 提取和更新都调

3.3 新增能力(逐条)

  1. 外部持久化存储:记忆大小受向量库容量约束,不受窗口约束。
  2. 自我更新:add/update/delete/noop 判定,会去重、改写错误、合并冲突,长期收敛。
  3. 按相关性检索而非按顺序保留:解决"早期关键约束被滑窗丢掉"的问题。
  4. 作用域隔离user_id/agent_id/run_id 过滤,多用户多 agent 互不污染。
  5. 图记忆:实体-关系存 Neo4j,支持关系查询和多跳推理;向量找"相似",图找"关系"。
  6. 程序性记忆:存"如何完成某任务"的可复用步骤。
  7. 基础设施抽象:LLM/embedder/向量库/图库可替换,可自托管或托管。

4. 三列总表 + DSH 强弱

维度 LangChain(旧 memory 类) LangGraph(checkpointer + Store) DSH
存储模型 内存数组 state + checkpointer append-only 事件日志 + 派生 surface
删除 真删 RemoveMessage / checkpoint replace,日志不删,带溯源
Token 计量 tiktoken 全量 无内建,自传 counter 增量折叠 + provider usage 校准 + projectedTokens
压缩 自由文本摘要,无收敛保证 summarize_conversation 自由文本 结构化 8 章节 + 可合并 + 收敛校验 + 工具配对安全
KV 缓存 一等约束:前缀稳定 + 重放复用
裁剪 trim_messages 通用 trim_messages 通用 专治 tool result + 精确省略计数
溢出恢复 手写 手写 自动归一化 + 退避重试
崩溃一致性 checkpoint 恢复,无消息级事务 压缩锁/孤儿检测 + 工具调用合成恢复
长期语义记忆 VectorStore/Entity/KG memory Store + 语义检索 无(短板)

DSH 强在哪(窗口内/会话内的工程精度)

  1. 删除 = replace 而非物理删,日志可回放可审计(事件溯源 + surface + sourceEventSeqs)。
  2. 增量 + provider 校准的 token 计量(replay-aware 折叠、复用真实 usage 分桶、projectedTokens)。
  3. KV 缓存是一等约束(前缀逐字节稳定、压缩前缀重放、记录失效点)。
  4. 压缩可收敛、可合并、事务化(结构化模板、必须短于原文、工具配对安全、start/summary/end + 孤儿锁、崩溃合成恢复)。
  5. 溢出自动恢复 + provider 级重试。

DSH 弱在哪(面试时主动说) :没有长期语义记忆、没有多用户作用域隔离、没有向量/图检索------这三块是 LangGraph Store + StoreSemanticSearch 和 mem0 的地盘。


5. 分层架构与正确组合

flowchart TB subgraph WIN[&#34;短期/工作记忆(窗口内,DSH 强项)&#34;] W1[&#34;token 计量&#34;] --> W2[&#34;压缩/裁剪/滑窗&#34;] W2 --> W3[&#34;溢出恢复&#34;] W1 --> W4[&#34;KV 缓存感知组装&#34;] end subgraph EXT[&#34;长期记忆(窗口外,mem0/LangGraph Store)&#34;] E1[&#34;提取 + 更新&#34;] --> E2[&#34;向量/图检索&#34;] end E2 -- &#34;按需注入 top-k&#34; --> WIN

核心结论:mem0 检索出的记忆最终仍作为文本进窗口,所以它不替代 token 计量/压缩/KV 缓存;两层互补,不是二选一。

一句话总结

LangChain 的记忆是"内存数组 + LLM 摘要";LangGraph 是"checkpointer 持久化 state + Store 跨线程存储";DSH 是"append-only 事件日志 + 派生 surface + 增量 token 计量 + KV 缓存感知的压缩"。 DSH 在单会话内的审计、一致性、缓存、溢出恢复上做到传统框架没有的精度;但没有长期语义记忆层,需要 mem0 或 LangGraph Store 补上。正确架构是分层组合。


6. 面试高频追问 + 参考答案

Q1:投影是什么?原值如何查找到投影? 答:事件溯源里,投影 = 把事件流折叠成的可重算读模型。DSH 的 surface(日志 → messages)和 session-projection(init/apply/view → UI 读模型)都是投影。正向用 sourceEventSeqs(投影条目 → 原始 seq),反向靠 replace 事件的 span 覆盖反查(原值 → 覆盖它的替换事件)。

Q2:KV 缓存是什么?如何做/存/查? 答:分两层。推理层 KV cache 是生成时缓存已算 token 的 K/V 避免重算;跨请求 prompt cache 是 provider 缓存 prompt 前缀 K/V,新请求前缀一致就复用。服务端按前缀哈希存、带 TTL;客户端不能直接查,只能看 usage 字段(cache_read/cache_write)。客户端配合命中靠:静态放前、排序固定、只 append、压缩重放前缀。

Q3:最近尾巴是什么? 答:压缩时保持原文不动的那段最近消息,由 retainRatio(默认 0.16)或 retainTokens 决定;被总结的是"最老 span = 全部 surface − 尾巴 − 未闭合单元"。尾巴必须留原文,因为最近状态需要精确措辞。

Q4:这么多处理点,什么时候做什么的依据? 答:唯一度量源是 tokenMeter.measure() vs 路由模型窗口容量 vs 阈值。三个触发点(pre-step 压力 / request-error 溢出 / 手动 compact),动作按"裁剪(无 LLM)→ 重测 → 压缩(调 LLM)→ 收敛重试"的省钱顺序走,溢出走独立强制路径。

Q5:mem0 和上下文管理(压缩/裁剪/保留N)是什么关系? 答:上下文管理是窗口内有损压缩(单会话、按顺序/结构取舍);mem0 是窗口外长期记忆(跨会话、按相关性检索、可 add/update/delete 收敛)。mem0 检索出的记忆最终仍进窗口,所以不替代压缩/KV 缓存,两层互补。

Q6:为什么要"事件溯源 + replace"而不是直接删消息? 答:直接删无法审计/回放/fork;事件日志 append-only + surface 投影 + replace,旧事件仍在日志、只影响投影,换来确定性重放和崩溃修复,且 sourceEventSeqs 保留溯源。


7. Agent 路由与模型路由

两类路由解决"请求给谁"和"发给哪个模型"两个问题,都发生在请求派生之前,决定后续的窗口预算、缓存与注入面。

7.1 Agent 路由(请求分给哪个 agent/技能/管线)

策略 做法 适用
规则/关键词 关键字、正则、前缀命令(如 /compact)命中即分发 简单、确定、零成本
LLM 语义路由 模型输出结构化分类(function calling / JSON),匹配各 agent 的 routing profile(描述+工具集+权限) 意图模糊、开放输入
嵌入语义路由 输入与各 agent 描述向量化,相似度 top-k + 阈值 不调 LLM、便宜、快
混合 规则/嵌入先走快路径,低于置信阈值再 LLM 兜底;仍低则反问澄清或回退默认 agent 生产最常用
flowchart LR IN[&#34;用户输入&#34;] --> R[&#34;规则/关键词 快路径&#34;] R -->|命中| AG[&#34;目标 agent/skill&#34;] R -->|未命中| E[&#34;嵌入相似度 top-k&#34;] E -->|置信 ≥ 阈值| AG E -->|低于阈值| L[&#34;LLM 兜底分类&#34;] L -->|分类成功| AG L -->|仍低置信| Q[&#34;反问澄清 / 回退默认 agent&#34;]
  • 每个 agent 必须有 routing description("我能做什么")与 fallback;DSH 的 skill 加载就是"任务匹配技能名"的按需路由。
  • 工具:Semantic Router(aurelio-labs)、LangGraph supervisor / handoff、OpenAI Swarm(多 agent 交接)。

7.2 模型路由(请求发给哪个模型)

策略 做法
静态映射 任务类型 → 模型写死在 adapter,最简单
能力路由 按请求需求查模型能力:上下文容量、工具调用、视觉、推理模式;DSH 在路由时 resolveModelInfo().context 声明容量,token-meter 拿它当预算基准
级联/升级 小模型先行,失败/拒答/低置信升级大模型(Not Diamond 思路)
成本/延迟感知 简单任务派小模型、复杂派大模型;偏好有 prompt cache(cache_read 更便宜)的 provider
可用性路由 按错误码 failover(rate limit → 换 provider)+ 退避重试;DSH llm-retry 即按错误码分类重试
  • 工具:LiteLLM(统一网关:model map、fallback、retry、预算)、Portkey(网关 + canary)、OpenRouter(托管路由)、OpenAI Router(开源语义路由)、LangChain with_fallbacks

7.3 与上下文管理的关系

  • 模型路由决定窗口预算measure() 的压力值必须与"被路由到的那个模型"的 context 容量比较;容量不写死、路由时解析,所以 thresholdRatio/retainRatio 参数不随模型变化。
  • 路由决定缓存与计费面:选哪个 provider 决定有没有 prompt cache、cache_read 单价,反过来影响"前缀稳定"值不值得做。
  • Agent 路由决定注入面:分给不同 agent = 不同 system prompt + 工具 schema,前缀逐字节稳定以 agent 为单位分别维护。
相关推荐
wangruofeng3 小时前
DeepSeek 识图上线:官方 Chat、Harness、cc-switch 到 Obsidian 保姆级配置
人工智能·aigc·deepseek
MicrosoftReactor3 小时前
技术速递|GitHub Copilot App 入门指南:管理你的工作
ai·github·copilot·agent
七夜zippoe3 小时前
OpenClaw 边缘部署实战:IoT 场景下的轻量级 Agent 集群与离线自治
agent·集群·lot·轻量级·边缘部署·openclaw
猿究院--Cu-Sn合金3 小时前
DeepSeek Harness 0.1.1-rc.1 发布:我的插件和启动器已完成适配
开源·deepseek
冬奇Lab5 小时前
开源项目第194期:deepseek-harness — DeepSeek 出品的 AI Agent 开发框架,万物皆插件
人工智能·开源·deepseek
Flynt5 小时前
DeepSeek终于能看图了:V4 Flash Vision实测,1分钱9张图但有个大坑
agent·ai编程·deepseek
闲猫5 小时前
LangGraph / Capabilities / Stores
python·agent·langgraph
yuhaiqiang5 小时前
从这两件事就能看出 vibecoding 距离专业作品差距有多大?AI 能抹平技术,但抹不平品味 !
前端·后端·程序员
天涯明月19935 小时前
AI Agent应用深度研究报告
人工智能·大模型·agent