跳到主要内容

05 · 上下文 / RAG / 压缩

这章讲 Chatbox 的“上下文工程”:历史消息怎么拼、上传的文件怎么进 prompt、知识库怎么向量检索、以及对话太长怎么自动压缩。这是把“能用”做到“好用”的关键一层。

5.1 三件事,一条流水线

喂给模型前,消息要经过三道加工,都在纯函数 buildContext(src/shared/context/builder.ts:11)里:

原始消息

├─ applyCompaction ← 有压缩点就从摘要后接续;旧轮次丢掉 tool-call
├─ filterErrorMessages ← 丢掉报错消息
├─ applyMessageLimit ← 按 maxContextMessageCount 截断历史(保留 system + 最后一条)
└─ injectAttachments ← 把文件/链接正文塞进消息文本

5.2 旧轮次丢工具调用(省 token 的小聪明)

工具调用的 argsresult 往往很占 token,但几轮之后就没意义了cleanToolCalls(builder.ts:87)只保留最近 keepToolCallRounds(默认 2)轮的工具调用,更早的消息把 tool-call 片段剔掉(removeToolCallParts, builder.ts:128)。

“轮”的边界由 findRoundBoundaryIndex 从后往前数 user→assistant 对(builder.ts:102-126)。

5.3 附件:内联 vs 工具读取 vs 检索

这是 injectAttachments(builder.ts:166)最有料的分支。同一个上传文件,有三种进上下文的方式:

模式何时怎么进 prompt
全文内联文件 ≤500 行,或模型不支持 read-file 工具整段包在 <ATTACHMENT_FILE><FILE_CONTENT>…
预览 + 工具补读文件 >500 行 模型支持 read-file只放前 100 行,加 <TRUNCATED> 提示用 read_file(第4章)
仅检索(RAG)附件标了 ragMode: 'session-retrieval'不放正文,只放占位 + <SYSTEM_REMINDER> 让模型用检索工具

看“预览”分支的实现(builder.ts:289-314):

// 示意,非源码:大文件 + 支持工具 → 只给预览,引导模型按 FILE_KEY 去读
const shouldTruncate = modelSupportToolUseForFile && fileLines > 500
const body = shouldTruncate ? lines.slice(0, 100).join('\n') : content
if (shouldTruncate) suffix += `<TRUNCATED>… Use read_file with FILE_KEY="${key}" …</TRUNCATED>`

直觉:支持工具的强模型,给它"目录 + 按需读";不支持的弱模型,只能一次性灌全文。 上下文工程随模型能力自适应。

仅检索模式的占位还附了一段 <SYSTEM_REMINDER>,明确告诉模型:这个文件是索引过的,问它就用 query_session_attachment + read_session_attachment_parents,无关问题就正常答(builder.ts:335-342)。

5.4 知识库 RAG:两段式检索

知识库(Knowledge Base)是持久化的文档向量库,跑在 main 进程,用 Mastra(@mastra/rag + @mastra/libsql)。

入库(src/main/knowledge-base/file-loaders.ts:80+):解析文件 → MDocument 分块 → embedMany 批量向量化 → vectorStore.upsert 存进 libSQL 向量表。embed 也 maxRetries: 0(计费安全,同第1章原则)(file-loaders.ts:168-173)。

检索(searchKnowledgeBase, file-loaders.ts:405)是经典两段式:

query ──embed──► 向量


vectorStore.query(topK=20) ← 第一段:向量召回 20 条(宽召回)


rerank(results, query, topK=5) ← 第二段:重排序精筛 5 条(精排)

▼ rerank 失败则回退用原始 20 条的前若干
返回 {id, score, ...metadata}
// 真实流程精简自 file-loaders.ts:416-432
const results = await vectorStore.query({ indexName, queryVector, topK: 20 })
const reranked = await rerank(results, query, rerankInstance, { topK: 5 })

召回宽、重排精是 RAG 的标准配方:向量召回快但糙,rerank 模型慢但准,先宽召回再精排兼顾二者。rerank 失败有 try/catch 回退到纯向量结果(file-loaders.ts:439-452),保证检索不因 rerank 不可用而整体失败。

模型侧拿到的是四个工具:query_knowledge_base(语义检索)、get_files_metaread_file_chunks(读具体 chunk)、list_files(toolsets/knowledge-base.ts)。description 里同样写了“greeting/闲聊别检索”的引导。

5.5 精华:超长对话自动压缩

对话越长越烧 token,最终撑爆上下文窗口。Chatbox 的解法是 compaction(压缩):到阈值就让模型把前文总结成一条摘要,之后只带摘要 + 摘要点之后的消息。

何时触发(packages/context-management/compaction-detector.ts:31):

isOverflow = 当前tokens > (contextWindow - 输出预留) × threshold

默认 threshold = 0.6(compaction-detector.ts:5)——即用到上下文窗口的 60% 就开始压,留足生成空间;未知上下文窗口的模型则不压(compaction-detector.ts:35)。

怎么压(stores/taskCompaction.ts:98 runTaskCompaction):generateSummaryWithStream 生成摘要 → 造一条 isSummary: true 的 assistant 消息 → 记一个 compaction point(边界消息 id + 摘要消息 id)(taskCompaction.ts:126-159)。

下次 buildContextapplyCompaction(builder.ts:43)读到这个 point,就从摘要开始接续,丢掉边界之前的原文(builder.ts:54-75):

// 示意,非源码:压缩点之后 = 摘要 + 边界后的消息
contextMessages = [summaryMessage, ...messagesAfterBoundary]
if (systemMessage) contextMessages.unshift(systemMessage) // system 永远保留

类比:像把一长串聊天记录"折叠"成一句会议纪要,旧细节进了纪要,新对话接着纪要谈。compaction point 就是那条折叠线。

5.6 边界

  • compaction 是有损的:摘要丢细节,模型可能"忘记"早期具体内容。
  • 知识库 rerank 依赖配了 rerank 模型;没配就只有向量召回,精度下降。
  • 附件全文内联对 ≤500 行文件是"一次性"的,大量小文件仍可能堆爆上下文。

5.7 代码地图

主题文件符号
上下文构建主线src/shared/context/builder.tsbuildContext
旧轮丢工具调用src/shared/context/builder.tscleanToolCalls / findRoundBoundaryIndex
附件三模式注入src/shared/context/builder.tsinjectAttachments / buildAttachment / buildRetrievalAttachment
知识库入库src/main/knowledge-base/file-loaders.ts(chunk + embedMany + vectorStore.upsert)
知识库检索(召回+重排)src/main/knowledge-base/file-loaders.tssearchKnowledgeBase
reranksrc/shared/models/rerank.tsrerank
压缩触发判定src/renderer/packages/context-management/compaction-detector.tscheckOverflow
压缩执行src/renderer/stores/taskCompaction.tsrunTaskCompaction / needsTaskCompaction