WeKnora 上下文管理

18 阅读6分钟

多轮对话的上下文来自消息表,会话侧不用图数据库做记忆。智能推理/快速问答两条路径都会「从库取出历史 → 拼进本轮输入」,但切分方式与超长策略不同:

智能推理快速问答
历史怎么切系统提示 / 历史中间段 / 当前用户轮;历史每轮再展开工具轨迹最多 N 对 {Query, Answer},再拼本轮 UserContent
默认保留约 5 轮(HistoryTurns)会话配置的 MaxRounds(≤0 则关闭多轮)
超长怎么办两级压缩:约半窗先 LLM 摘要合并中间历史 → 约八成再按工具消息组机械裁掉最旧;系统与当前轮始终不动按轮硬丢:超过 MaxRounds 的更早问答对直接不进模型;已留下的 N 对不再按 token 做摘要或二次裁剪

下面先按路径展开「分成哪些部分」和各自的超长策略,再介绍加载上限与检索是否跨轮。

智能推理:三段式 + 两级压缩

引擎拼装时大致是:

[1] system          系统提示(本轮现算,不从历史里复用旧 system)
[2] 历史中间段      最近 N 个已完成问答轮展开后的 messages
[3] 当前用户轮      本轮问题(及本轮后续的 assistant / tool,随 ReAct 增长)

其中 每一轮历史 再拆成:

user                         当时的用户原文(可带图注、附件;故意不用旧的 RenderedContent,避免把过时 RAG 信封混进当前协议)
assistant + tool_calls       AgentSteps 里的中间步(思考文案 + 工具调用)
tool(可多条)               与上面 tool_calls 成对的结果
…(可有多组中间步)
assistant                    去掉 <think> 后的最终回答

超长压缩只动「历史中间段」:系统提示与当前用户轮(及之后本轮工具轨迹)始终保留。每轮调模型前估计 token(优先上一轮 API Usage,再加新增消息增量),然后:

  1. 摘要合并(约超过窗口一半):把较早的中间历史交给 LLM 收成一条系统侧 [Memory Summary…],较近历史与当前轮原样留下;摘要失败则改为原文归档,避免一次坏压缩毁掉上下文。
  2. 机械裁剪(仍超过约八成):把中间历史按「assistant(tool_calls) + 对应 tool 结果」成组,从最旧一组开始丢掉,不拆断 Function Calling 配对。
// internal/agent/observe.go
if e.memoryConsolidator != nil && e.memoryConsolidator.ShouldConsolidate(currentTokens) {
    consolidated, err := e.memoryConsolidator.Consolidate(ctx, messages)
    // 失败则继续走机械压缩
}
messages = agenttoken.CompressContext(messages, e.tokenEstimator, e.config.MaxContextTokens, currentTokens)

加载时另有轮数上限:LoadAgentHistory 默认约 5 轮,库表为唯一来源,每轮问答前加载一次再交给引擎。

快速问答:问答对 + 轮数封顶

库里同样是按 RequestID 存的 user / assistant 消息行,但快速问答不会像智能推理那样把 AgentSteps 展开成 assistant(tool_calls) + tool 序列。LOAD_HISTORY 先聚成内存结构 History,再压成普通多轮对话:

History 每条 ≈ {
  Query               ← 用户侧 Content(可附带历史图注、附件文案;不用旧 RenderedContent 当 Query)
  Answer              ← 助手最终回复(去掉 <think>…</think>)
  KnowledgeReferences ← 该轮引用块(可选,供本轮合并引用,不是 tool 消息)
}

拼进模型时由 AppendHistoryMessages 变成严格的两行一组:

user:      history.Query
assistant: history.Answer
…(最多 MaxRounds 组)
user:      本轮 UserContent(模板拼好的「检索上下文 + 当前问题」)

因此模型看到的历史里没有 role=tool,也看不到当时检索/工具的中间过程——只有「当时问了什么、最终答了什么」;引用靠旁路字段 KnowledgeReferences 再并进本轮,而不是重放工具轨迹。

加载细节:按 RequestID 聚对 → 去掉思考标签 → 按时间取最近 N 对再正序写入。MaxRounds <= 0 关闭多轮,历史整段不进模型。

超长策略 = 按轮丢弃,不是摘要压缩:

  • 问答对数大于 MaxRounds 时,更早的轮次直接丢掉(不进本轮模型上下文);库里的消息仍在,只是本次不加载。
  • 没有智能推理那套「半窗 LLM 摘要 + 八成成组裁剪」:已经留下的 N 对不会再因窗口将满而摘要或按 token 二次裁掉中间内容。
  • 体积主要靠 MaxRounds 与提示词模板约束;检索/网页正文另有 top-K、长度截断,那是材料侧,不是对话历史的 Memory Summary。排障时不要按 Agent 压缩去对这条路径。

是否把上一轮「拼装后的检索正文」带回来,取决于是否把渲染结果写回消息字段(与 Agent 默认涂掉旧检索不同,见下节)——即便库里存了 RenderedContent,快速问答进历史时仍以用户 Content 为 Query,避免把上一轮整段「<context id="…">检索块</context> + 当时引用说明」原样塞进本轮,和当前引用格式(如 <ref id="cN"/>)打架。

另有隐藏的聊天记录知识库,可把问答对写入供搜索接口用,但不会自动注入任一条路径的提示词。

检索结果是否跨轮

  • 智能推理:默认把历史里知识库类工具(语义搜、grep、图谱、Wiki 读等)的 tool 结果换成「请重新检索」类占位,避免知识库变更后仍吃旧块;打开 RetainRetrievalHistory 才原样保留。
  • 快速问答:可将本轮拼装好的 UserContent 写入渲染字段,下一轮历史仍可能带上当时资料——与 Agent(智能推理) 默认方向相反,需按场景选用。

提升了什么

智能推理把「轮数上限 → token 阈值下的摘要 → 成组硬裁」叠在同一套三段切分上,窗口贴模型真实消耗,又保住 tool 协议与当前问题。快速问答用更简单的轮数模型,路径短、行为可预期。两条路径策略分开写清,避免把 Agent 压缩误套到 RAG 链上。

好处与代价

好处

  • 三段切分让压缩作用域可读:永远保住系统设定与当前问题。
  • 摘要失败有原文归档回退;机械裁剪不拆 tool 对。
  • 用 API usage 估预算,触发点更贴近实际消耗。

代价

  • 长对话更慢、更贵:摘要要调模型;默认涂掉检索又可能多一轮重搜。
  • 涂掉工具结果后,模型可能重复检索相近内容。
  • 快速问答靠硬丢旧轮控长:超 MaxRounds 的对话对模型不可见,无摘要兜底;极长多轮只能调小轮数。若渲染字段带着过时检索,还会把旧资料带进新问题。

基于 WeKnora v0.7.0

上一篇:WeKnora 问答实现 | 下一篇:WeKnora 回答溯源