Cherry Studio 上下文构建/压缩运行时问题排查实录:从 `tool_invoke` 400 到实体级 Tool Output Codec 根本修复 📅 发布时间:2026/9/19 19:41:48 👁 浏览次数: Cherry Studio 上下文构建/压缩运行时问题排查实录从tool_invoke400 到实体级 Tool Output Codec 根本修复【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio本文基于 Cherry Studio 仓库v2-refactor-temp/docs/ai/2026-08-02-context-build-runtime-test-findings.md展开并结合当前仓库源码src/main/ai/tools、src/main/ai/contextBuild、src/main/ai/streamManager/persistence、packages/aiCore/src/core/context、src/shared/ai/transport等进行源码级印证与扩充。导读Cherry Studio 在长程工具任务20 步read_file循环、多轮联网搜索与压缩compaction机制共同作用下暴露出 7 类运行时问题从 Anthropic 端点因tool_invoke的inputExamples与序列化 schema 不匹配而整请求 400到压缩折叠附件后read_file失联、read 类工具回声全量占库、in-loop 压缩成本线性放大再到 trace 持久化崩溃与 MCP 解析非确定性。本文完整复述这些问题的现象、根因、修复方案与验证结果并深入剖析最终落地的「实体级裁剪Tool Output Codec」根本修复——把工具输出从不透明字节流升级为骨架 内容块的结构化裁剪模型。读完你将对 Cherry Studio 上下文构建的 in-flight 截断、persist 落库、压缩召回与前缀缓存四条链路有完整认识并掌握结构感知截断的设计原则与实现细节。测试环境与方法测试采用克隆档案CherryStudioCtxTest将上下文裁剪阈值从默认 5000 调至 100000、context_window缩至 16000配合 cherry-electron-dev 实例由真实模型aihubmix::claude-sonnet-4-6 / gemini::gemini-2.5-flash驱动长程工具任务。测试范围是feat/context-build-truncation分支 7f0a0fbd34其中 #1、#5、#6 为 main 既有问题与本分支无关。关于压缩的三种机制需要先厘清边界测试结论才有意义in-loop 压缩主循环某步超限时折叠旧轮次见packages/aiCore/src/core/context/inLoopCompaction.tsdurable 压缩turn 开始前一次性折叠 keep 边界之前的轮次见src/main/ai/contextBuild/PersistentChatContextProvider的resolveCompactedHistorypersist 裁剪v1message.data落库前对超大工具输出的裁剪见src/main/ai/streamManager/persistence/trimToolOutputs.ts。修复状态总览2026-08-03全部在本分支落地问题状态提交#1 tool_invoke schema 400✅ 已修复两层覆盖手写jsonSchema()绕过 provider-utils sanitizeSchema保留显式additionalProperties:true2ec4f0c07f/ai-sdk__anthropic.patch#2 压缩折叠附件后 read_file 失联✅ 已修复权威附件清单随请求下发 摘要行附附件清单提示81a7e1ee37#3 read 类工具回声全量占库✅ 已修复read_file / fs_read persist 侧 text-field codec793b77885f#4 in-loop 压缩无记忆化✅ 已修复prepareStep 闭包内增量折叠缓存零重复摘要化8bfe78af9a#5 AiTurnTrace 持久化崩溃✅ 已修复non-recording span 不再进 convert/sinkspanConvert 加防御479d4a370a#6 MCP 解析非确定性✅ 已修复解析前 warm 目录缓存 三层 mcpMode 缺省统一manual9c349245e8#7 web 工具截断根本修复✅ 已落地 P1-P3实体级 codecP4 truncatable 布尔退役为后续项f406425279/c10902e244/793b77885f#1tool_invoke的 inputExamples 与序列化 schema 不匹配 → Anthropic 端点整请求 400严重度高main 既有 bug建议单独开 issue现象tool defer 触发auto 池 窗口 10% 等三条件后meta 工具进入请求Anthropic 系端点实测 aihubmix返回tools.N.custom: Example at index 0 is invalid: False schema does not allow cherry studio latest release. Each example must match the tools input_schema.整个请求失败。同一请求去掉 MCPdefer 不触发即成功换 Gemini 端点不校验示例defer 全链路正常tool_search/tool_invoke 均验证通过。根因第一层src/main/ai/tools/adapters/aiSdk/meta/toolInvoke.ts的inputExamples: [{ input: { name: web_search, params: { query: cherry studio latest release } } }]与params: z.record(z.string(), z.unknown()).optional()经 zod→JSON Schema 序列化后的结果不匹配。真正原因不在 zod 序列化本身——zod v4 产出的additionalProperties: {}是对的是ai-sdk/provider-utils的addAdditionalPropertiesToJsonSchema对每个 object 节点无条件覆盖additionalProperties: false把params变成不接受任何属性的死对象example 校验 400模型正常传参同样违反。修复第一层2ec4f0c07ftoolInvoke.ts改用手写jsonSchema()——asSchema对已包装 schema 原样放行跳过 provider-utils 的覆盖params显式additionalProperties: true运行时校验保留原 zodsafeParseinputExamples保留修后合法。当前源码中可看到这一实现const toolInvokeInputSchema jsonSchema{ name: string; params?: Recordstring, unknown }( { type: object, properties: { name: { type: string, description: Tool name as returned by tool_search }, params: { type: object, additionalProperties: true, description: Tool input arguments } }, required: [name], additionalProperties: false }, { validate: (value) { const result toolInvokeInputZod.safeParse(value) return result.success ? { success: true, value: result.data } : { success: false, error: result.error } } } )根因精化第二层patches/ai-sdk__anthropic.patch第一层修复后 aihubmix 端点仍复现同一 400。追因发现ai-sdk/anthropic的sanitizeSchema由本仓 patch 把sanitizeJsonSchema接入prepareTools后对每个工具input_schema生效在发送前又一次对每个 object 节点无条件result.additionalProperties false把 asSchema 已放行的params: additionalProperties:true重新压回false。asSchema 层provider-utils不再覆盖但 provider 自己的 sanitizer 覆盖——两层是独立的两处 clobber。修法改sanitizeSchema为result.additionalProperties schema.additionalProperties true ? true : false仅保留显式true其余仍默认闭合普通工具零影响。边界测试aihubmix.anthropicTools.test.ts新增用例钉住 wire schemainput_schema.properties.params.additionalProperties true且外层对象仍false——直接跑真实prepareTools→sanitizeJsonSchema序列化路径无需 Electron。影响面Anthropic 家族端点 auto 池过线默认 200k 窗口需 ≈20k tokens 的 MCP 工具描述约几十个工具小窗口模型更容易。多 MCP 重度用户会真实命中且表现为开了很多 MCP 后 Claude 突然全部请求报错。补充测试中 #1 再次命中且路径更真实重启后新话题自动继承助手绑定的 MCP即 #6 行为16k 窗口下 auto 池过线 → defer 触发 → aihubmixAnthropic API连续 6 个请求 400用户无任何显式操作即全灭取消话题 MCP 选择后恢复。修复方向任一对齐 example 与序列化后的 schema去掉inputExamplesanthropic provider 侧在发送前丢弃与 schema 不符的 examples。注意toolSearch.ts/toolInspect.ts的 examples 需一并核查——核查结果examples 全为已声明属性不中招。#2 durable 压缩折叠附件消息后read_file 失联、细节不可恢复严重度中本分支相关的交互缺口现象16k 窗口下第一轮20 页 read_file结束后第二轮 turn 开始触发 durable 压缩带附件的 user 消息被折进compaction_summary。此后served 消息里没有 file part →collectFileAttachments为空 →hasFileAttachmentsfalse→read_file 不再注册模型想重读附件也做不到实测模型自报工具列表无 read_file只能凭摘要回答细节实测答错问 LOG-0001 的 temperature答 119实际 21.1。对比工具输出有persisted-outputmarker fs_read 幸存通道信封渲染 历史 allow-list 注入跨压缩仍可读回普通附件没有对应通道。修复81a7e1ee37两条修复方向都做了——resolveCompactedHistory从 RAW 路径行收集权威fileAttachments清单经AiStreamRequest.fileAttachmentsmain 内部字段不过 IPC下发buildAgentParams优先取它read_file 注册 allow-list 与 served parts 解耦同时 durable 摘要行附加附件清单提示[Files attached in this conversation remain readable in full via the read_file tool: …]纯存储字段渲染字节稳定恢复模型的调用信号。位置PersistentChatContextProvider.resolveCompactedHistory折叠×buildAgentParams.collectFileAttachments从 request.messages 收集。#3 read 类工具回声全量占库v1 裁剪范围边界两轮测试均出现严重度中已知范围决策量化后建议提优先级现象测试 1模型跟随 marker 用 fs_read 读回 3 份全文fs_read 的结构化输出{kind,text,...}truncatable:false 非 string/mcp-content 形状不参与 persist 裁剪 → 95KB 消息中 ~66KB 是 fs_read 回声测试 220 个 read_file 结构化输出 169KB 全量入message.data。本质v1 裁剪范围只收 string 与全文本 MCP 信封见src/main/ai/contextBuild/toolOutputStore.ts的extractPersistableText仅纯字符串与 content 全为 text block 的 MCP 信封结构化 JSON、多模态 MCP 内容一律返回 null 保持全量read 工具fs_read/read_file的结构化输出即使巨大也全量入库。模型越勤快读回DB 省得越少。修复793b77885f经由 #7 codec P2/P3read_file 挂makeTextFieldCodec({textKey:text})persist 专属——其 toModelOutput 是 textin-flight 实体路径只认 json永不触发169KB 级页回声落库时text进 blob、分页字段留骨架fs_read 保留 in-flighttruncatable:false防循环 同款 codec 走 persist lane——默认配置下不触发输出 cap persist 阈值裁剪门限为严格阈值调低时才裁重复读同页经 contentHash 收敛到同一 echo blob。注意一个实现细节echo 带 cat -n 行号格式见FsReadTool.ts的formatLines6-pad 行号 tab不会命中源 blob 的 contentHash——设计稿中零额外存储不成立重复读同页仍收敛到同一 echo blob而非源 blob。#4 in-loop 压缩无记忆化的成本放大代码已注明 accepted cost此处量化实测严重度低-中优化项实测16k 窗口、20 步 read_file 循环中主循环步输入被正确压至 9-11k tokens但每个超限步全量重折叠旧轮次摘要化调用输入 44k→50k→60k→66k 递增。整轮 20 请求共 446k input tokens$0.83prompt cache 吸收 cacheRead 252k。修复8bfe78af9aprepareStep 闭包内缓存{consumedCount, compactedPrefix}超限步先构造[...compactedPrefix, ...messages.slice(consumedCount)]——低于触发线直接复用零 LLM 调用仍超限才折叠增量并更新缓存摘要折摘要与 durable 语义一致。摘要化调用输入从 O(全历史) 降到 O(增量)44k→66k 的递增消失。此前inLoopCompaction.ts头注释已标 no memoization in v1。#5 turn 以 ToolLoopTerminalError 终止时 AiTurnTrace 持久化崩溃严重度低main 既有观测性缺口现象WARN [AiTurnTrace] Failed to persist root span ai.turn TypeError: Cannot read properties of undefined (reading 0) at AiStreamManager.onExecutionError——恰好在最需要 trace 的异常终止路径上丢了 trace。根因修正原记录异常终止路径是采样偏差——真实根因是developer mode 关闭时没有 TracerProviderstartSpan返回 NonRecordingSpan无startTimeend 补丁无条件convertSpanToSpanEntity→span.startTime[0]抛 TypeError。每次 turn 结束都崩与 outcome 无关只是 WARN 淹没在正常日志里、异常终止时才被注意到。修复479d4a370a双保险AiTurnTraceend 补丁对无startTime的 span 直接 no-op 返回不 convert 不写 sinkspanConvert.ts补 startTime 防御与既有 endTime 守卫同风格。新增无 provider 用例handle.end()不 throw、sink 不被调。#6 mcpToolIds 空数组不回落到助手绑定行为待确认可能按设计严重度待确认前提修正原记录的渲染端传空数组前提不成立——聊天 IPC schema 根本没有mcpToolIds字段composer 的 MCP 选择器写的是助手级mcpServerIds聊天请求恒走resolveAssistantMcpToolIds回落。非确定性另有两源McpCatalogService.listToolscache-only——冷缓存返回[]只触发异步预热空结果还有 5 分钟退避重启后首个话题 vs 后续话题因此不一致三层 mcpMode 缺省不一致mainmanual/disabled、shared DEFAULTauto、rendererdisabled。修复9c349245e8解析前对目标服务器await warmToolsCache(server.id)冷缓存不再静默空集三层缺省统一为 sharedDEFAULT_MCP_MODE manual。新增 resolveAssistantMcpTools 确定性测试。附测试中确认不是 bug 的现象第一轮长循环以ToolLoopTerminalError20 步工具上限终止 —— 步数护栏按设计。gemma 免费层配额报错 —— 外部配额限制。压缩后细节回答不精确本身是摘要化的固有代价#2 记录的是想重读也读不到的通道缺失。补充测试网络搜索 × 压缩召回率 × 前缀缓存命中AI_SDK_DEVTOOLS1场景16k 窗口aihubmix::claude-sonnet-4-6前两轮 gemini-2.5-flash4 轮联网搜索巴黎奥运金牌/C919 航程/SQLite 版本/珠峰高程 4 轮禁搜索召回测验devtools 捕获全部 24 个请求载荷于.devtools/generations.json。结论 1web 搜索结果没有持久化/截断通道设计如此记录为潜在跟进web_search/web_fetch均truncatable: false引用工具citation 抽取需要原文——搜索结果双份全量出站 prompt 内联 message.data全量唯一的瘦身通道是压缩层折叠旧轮。另有独立的chat.web_search.compressionmethod/cutoff_limit本档案为 none在结果进入上下文前做源级压缩与 context-build 无关。若给 citable 工具开 persist 通道需先解决 citation 与 marker 的共存。✅ 2026-08-03 起已过时#7 codec P1-P3 落地后web_search/web_fetch/kb_search 均走实体级截断持久化citation 骨架落库渲染端从 skeleton 解析源级压缩默认也翻为 cutoff新装。结论 2durable 压缩对搜索轮生效prompt -68%写一次服务多次搜索 4 轮后估算过线durable 压缩在 turn 开始触发一次边界行写入 2,391 字符摘要耐人寻味边界行是一条 error 消息收尾时仍被正确选中。devtools 实测出站 prompt 从 28KB 降到 9KB-68%之后 8 个请求全部复用同一摘要行无重复摘要化调用对照 in-loop 的每步重折叠durable 是 write-once-serve-many。结论 3召回测验 4/4 全对含被折叠轮次被折叠的 R1巴黎奥运 40 金精确召回模型自述根据对话摘要中保留的信息 ✅被折叠的 R2C919精确复述了失败情形页面内容为空、无 web_fetch、未给最终数字摘要甚至保留了备用知识 5555km 但当时未作为答案的区分 ✅未折叠的 R3/R4SQLite 3.51.0 / 2025-11-04、珠峰 8848.86 米逐字召回 ✅摘要质量注记摘要为结构化英文 digest✅ Completed / ❌ Failed/Incomplete / Context to Preserve对数字型事实的保真明显好于 read_file 测试中对 2000 行日志明细的保真#2 的 LOG-0001 答错——摘要保真度与信息密度强相关事实型 QA 场景召回率高海量明细场景仍需读回通道。结论 4前缀缓存命中率 ≈99.9%字节稳定的直接证据压缩启用后连续 9 个成功请求的 Anthropic usage每步noCache仅1-3 tokenscacheRead3,087→4,604 递增cacheWrite只覆盖新增后缀——摘要行渲染 全量 web 结果在多轮间字节完全稳定provider 前缀缓存几乎满命中。Gemini 两轮的隐式缓存命中率约 60-80%。过程中复现的既有问题#1 再次命中且路径更真实重启后新话题自动继承助手绑定的 MCP即 #6 行为16k 窗口下 auto 池过线 → defer 触发 → aihubmixAnthropic API连续 6 个请求 400用户无任何显式操作即全灭取消话题 MCP 选择后恢复。anthropic 直连与 gemini 免费层的配额/余额错误为外部因素gemini free tier 5 req/day。#7 web_fetch/web_search 是否应参与截断——分析与分层建议性质改进方案承接补充测试结论 1web_fetch 建议尽快做豁免现在真正保护的两层In-flight 引用诚实性两工具输出为[{id:prefix-n, title, url, content}]JSON 数组webLookup.tsmapResponse模型靠看见的条目回写[cite:id]。截断器对 json 是 stringify 后掐 head/tail——cite id 与 content 的映射会被从中间切碎模型对未见条目要么弃引信息损失要么幻引更糟。落库后的 citation 渲染src/renderer/utils/message/citations.ts的 citation registry就地从 message.data 的工具输出 parts 解析no persisted reference metadata。输出被信封替换后历史消息的角标/来源卡/导出/复制全部失解。现状的真实风险实测支撑压缩层从不折当前 turndurable 只折 keep 边界前in-loop 至少保一 turn——当前轮一发超大 fetch 结果没有任何防线。web_fetch的 readable content 无工具级上限长文档页 50-200k chars是目前唯一完全裸奔的上下文洪水源searchmax_results5×snippet实测单轮仅 3-6k chars。双份全量落库重复搜索不去重。源级压缩chat.web_search.compressionpostProcessing.tscutoff/rag存在但默认 none。分层建议web_fetch应改为可截断优先做。引用身份是 URL 且在input里result 骨架也有截 content 不损失引用身份内容为单篇正文head/tail marker fs_read 读回与 filesystem read 语义同构又是最大单发洪水源。in-flight 翻 flag 即止血persist 侧待shape:json#3配 citation-aware 信封保留{id,url,title}骨架。web_search不建议裸翻 flag。默认 100k 阈值下几乎永不触发no-op触发时伤的恰是引用结构。正确杠杆是给源级 cutoff 一个温和默认值逐条裁 content、天然保留每条 id/url/title而非动 truncatable。终局抽象结构感知截断。布尔 flag 表达不了 citable 工具的需求——按 result 条目为单位裁 content、永不裁引用骨架。可在 truncatorperTool上扩展 per-tool 自定义 reducer让 web_search/web_fetch/kb_search 都能安全参与截断持久化。行动排序① web_fetch 翻 flag一行立即止血→ ② search 源级 cutoff 默认值 → ③shape:json citation-aware 信封与 #3 合并→ ④ per-tool reducer。量级 sanity check200k 正常窗口下 search 需 ~100 轮才顶满压缩层足够fetch 一发长页即可 50k——优先级由此而来。✅ 已落地升级为下节的 codec 方案未走裸翻 flag路线web_fetch 直接上实体 codecP1f406425279而非布尔翻转web_search/kb_search 同 codecP3793b77885f默认阈值下近 no-op、纯保险网源级 cutoff 翻 schema 默认none→cutoff经 classification 重新生成仅新装生效存量迁移写入的显式值不动。citation 共存由 P2 的 skeleton 落库 渲染端 skeleton 解析解决。根本修复设计内容/结构分离的「实体级裁剪」Tool Output Codec根因一句话现在的截断把工具输出当不透明字节流extractText → head/tail而工具输出实际是有结构的——身份字段id/url/title 大体积内容字段。字节级掐断毁结构整工具豁免弃防线布尔truncatable表达不了裁内容、保骨架。根本修复 把裁剪单位从字节流改成实体的内容字段。核心抽象每个工具注册一个输出编解码器codec设计稿接口定义在src/main/ai/tools/adapters/aiSdk/types.tsToolEntry 声明interface ToolOutputCodecTOutput { /** 拆分骨架(身份/引用字段,永不裁) 可裁文本块(每实体一块) */ deflate(output: TOutput): { skeleton: unknown; blobs: Array{ key: string; text: string } } | null /** 重建骨架 全文块 → 原始输出(渲染端展开 / ai.tool.get_result 用) */ inflate(skeleton: unknown, blobs: Recordstring, string): TOutput } // 未注册 codec 现状 opaque(字符串/全文本 MCP 走既有 head/tail); // exempt 仅留给真正永不裁的场景(如 fs_read 的 in-flight 防循环豁免)。预置 codecweb_search / web_fetch / kb_searchentities形——骨架 [{id,url,title}]blobs 各条content。超阈值只裁单条 content 为 head/tail marker引用骨架在 prompt 与 DB 双侧永不有损fs_read / read_file#3 顺带解决json-text-field形——骨架 {kind,startLine,...}blob text字段。fs_read 保留 in-flight 豁免防循环但persist 侧裁回声整页读回的 blob 经 contentHash 去重直接命中它刚读的那个 entry零额外存储见下节偏差记录实际因行号格式不命中源 blob。当前仓库中的实际实现src/main/ai/tools/outputCodec.ts提供了两个 codec 工厂makeEntitiesCodec({ contentKey })面向[{…identity, [contentKey]: string}]形态web_search / web_fetch / kb_search 结果每个实体的 content 是 key 为/index/contentKey的一个 blob其余全部留在骨架makeTextFieldCodec({ textKey })面向单记录 单个大文本字段fs_read / read_file 回声一个 blob key 为/textKey。两者共享assemble语义浅克隆并保持键插入顺序——组装后的值在两条 lane 上都会被 stringify 进 prompt必须字节稳定以维持前缀缓存命中。snippet使用CITATION_SNIPPET_MAX_CHARS截断内容用于骨架内联摘录。注意实现细节deflate对非目标形状错误对象、steer 字符串、marker返回null走不透明回退路径。统一管线两条 lane 共享一个裁剪原语设计稿管线如下trimToolOutput(toolName, output, budget) // 唯一入口,查 registry codec ├─ in-flight(truncator):裁后的实体渲染 per-entity marker → 出站 prompt └─ persist(trimToolOutputs):信封扩展为多 blob 形态 $persistedToolOutput: { shape: text | mcp-content | entities | json-text-field, skeleton, // 引用骨架原样落库 → citation registry 照常就地解析 blobRefs: [{ key, fileEntryId, vfsFilename, head, tail, totalChars, totalLines }] }in-flight 侧packages/aiCore/src/core/context/truncator.tsTruncateOptions.perTool新增codec?: EntityToolOutputCodec字段工具策略对象化后JSON 输出 codec 走truncateEntities先codec.deflate(output.value)得到骨架与 blobs逐 blob 判断text.length threshold headChars tailChars text.length决定是否裁剪含永不重复裁剪已持久化 marker守卫超预算且有 storage 时经Offloader.offloadAsync持久化换取 URI 标注的 head/tail marker无 storage 时走内联--- truncated (N lines, M chars total) ---形式最后codec.assemble(deflated.skeleton, texts)重组。deflate → null错误对象、steer 注记等回退不透明路径。extractText只负责不透明路径的文本抽取实体路径完全绕开它。persist 侧src/main/ai/streamManager/persistence/trimToolOutputs.tstrimOversizedToolOutputs与 in-flight 中间件同层取阈值globals assistant override 快照两 lane 用同一有效阈值builtin registry 有codec则走trimViaCodec超限 blob 各自落一个 FileManager entry骨架内联 snippet 后生成{ shape:entities, skeleton, blobRefs }信封无 codec 且truncatable:false则跳过否则走 v1 全文本 lanetrimWholeText。storage 失败 per-part 非致命——宁胖勿丢全量保留在 message.data。信封类型src/shared/ai/transport/persistedToolOutput.tsv1 单 blob 信封PersistedToolOutputSingleRefshapetext | mcp-content字段集冻结v2 多 blob 信封PersistedToolOutputEntitiesRefshape:entitiesskeletonblobRefs。blobRefsOf把两种信封统一成 blob 视图单 blob → keyenvelopeDisplayExcerpt为传输/渲染端提供首个 blob head 末个 blob tail 合计统计的形状无关摘录。$persistedToolOutput哨兵键是唯一判别依据。文件头注释明确了与deferredToolResult.ts仅传输层裁剪、DB 保留全文、渲染端回取的边界本模块 DB 本身只存摘录。内容寻址与生命周期src/main/ai/contextBuild/toolOutputStore.ts全文 blob 存放在 FileManager internal entrycleanupPolicy: delete_when_unreferenced由chat_message_file_refroletool_output把生命周期绑定到消息contentHash去重保证同一输出跨 turn / regenerate 只存一份computeVfsFilename用 sha256 前 16 位生成vfs_sha256[:16].txt与 in-flight offloader 的_generateFilename字节一致保证 marker URI 两 lane 相同isToolOutputBlobEntry提供消费侧的严格门禁origin internal 自动清理 txt 扩展名。每个 blobRef 一条tool_outputfile ref一消息多 ref 已支持fs_read 的 per-request allow-list 收集全部 blobRefs 路径ai.tool.get_result/ 渲染端展开走inflate。五条不变量「根本」的定义引用骨架永不有损——prompt 侧模型看全每条 id/url/title内容为摘录DB 侧 citation registry 解析不变单一裁剪原语——形状判断只写在 codec 里一处in-flight 与 persist 永不漂移今天的extractPersistableText/truncatorextractText双实现合并确定性渲染——marker/骨架为存储字段的纯函数字节稳定前缀缓存维持 ≈100% 命中已有契约测试模式直接沿用全文永可读回——每个被裁 blob 都有 marker fs_read 通道 UI 展开失败回退全量——codec 抛错则该输出原样落库宁胖勿丢沿用现有 per-part try/catch。落地阶段阶段内容消化的问题状态P1codec 接口 web_fetch entities codec仅 in-flight#7 最大洪水源止血✅f406425279P2多 blob 信封 skeleton 落库 inflate 读回/展开#7 persist 侧、citation 共存✅c10902e244顺带修了 topics GET 不投影导致的冷重载裸信封渲染 bugP3web_search/kb_search codec 源级 cutoff 默认值fs_read/read_file 回声 codec#3 全量消化✅793b77885fP4删除布尔truncatable迁移为 codec/exempt 声明truncator 的extractText与extractPersistableText合并进 codec双实现漂移风险清零⬜ 后续项本轮保留 truncatable 作 in-flight 豁免语义codec 与 flag 并存时in-flight preserve、persist 走 codec落地与设计的偏差记录实现用deflate/assemblein-flight 重组 通用spliceTextAtKey/inflateEntitiespersist 渲染/读回靠 blob key 的 JSON-pointer-lite 自描述不依赖 codec 存在替代了设计稿的deflate/inflate对信封 shape 只增entities一种json-text-field由单 blob 的 entities 信封覆盖key: /textfs_read 回声 blob 因 cat -n 行号格式不会命中源 blob 的 contentHash设计稿的零额外存储不成立重复读同页仍收敛到同一 echo blob。跨 lane 字节契约由测试钉死persist 渲染的 entities 输出与 in-flight truncator 输出JSON.stringify逐字节相等。触点清单供深入阅读src/main/ai/tools/adapters/aiSdk/types.tsToolEntry 声明codec 字段与 truncatable 并存packages/aiCore/src/core/context/truncator.tsin-flight 截断器perTool.codec实体裁剪路径src/shared/ai/transport/persistedToolOutput.ts$persistedToolOutput信封v1 单 blob v2 entities守卫向后兼容src/main/ai/streamManager/persistence/trimToolOutputs.tspersist 侧统一原语codec → entities 信封、无 codec → v1 全文本 lanesrc/main/ai/contextBuild/toolOutputStore.ts全文 blob 的内容寻址存储、去重与生命周期src/main/ai/tools/outputCodec.tsmakeEntitiesCodec/makeTextFieldCodec工厂实现src/main/ai/tools/webLookup.ts/FsReadTool.ts/src/main/ai/tools/adapters/aiSdk/builtin/ReadFileTool.tscodec 定义与 in-flight 豁免语义src/renderer/utils/message/citations.tsskeleton 解析预期零改动。结语四层防御协同后的稳定形态经过上述修复Cherry Studio 上下文构建形成了四层协作的稳定形态in-flight truncator实体级 codec 裁剪 marker→persist trimmer同一 codec 生成entities信封全文进 FileManager blob→fs_read/read_file 读回通道marker allow-list跨压缩仍可读回全文→compactiondurable 写一次服务多次、in-loop 增量折叠零重复摘要化。实测证据表明压缩启用后出站 prompt 可降 68%、前缀缓存命中率 ≈99.9%、事实型召回 4/4而「骨架永不有损、单一裁剪原语、确定性渲染、全文永可读回、失败回退全量」五条不变量则从设计层面杜绝了字节级截断对引用结构与可读性的破坏。若需深入某条链路可沿上文触点清单直接阅读对应源码与测试。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考