Skip to content

fix(provider): sort Claude tool results by tool use order to preserve prompt cache - #1167

Open
3316891527 wants to merge 2 commits into
AAswordman:devfrom
3316891527:fix/claude-prompt-cache-tool-result-order
Open

fix(provider): sort Claude tool results by tool use order to preserve prompt cache#1167
3316891527 wants to merge 2 commits into
AAswordman:devfrom
3316891527:fix/claude-prompt-cache-tool-result-order

Conversation

@3316891527

Copy link
Copy Markdown
Contributor

变更说明 / Description

在 Anthropic 协议开启 Tool Call 模式下,当上一轮包含多个并行工具调用时,用户发送新一条消息后,Claude 的 Prompt Cache 会从该轮工具调用开始的位置整体失效,工具历史全部变成 cache_creation 重新计费。

本 PR 在 ClaudeProvider 构建序列化消息历史时,将匹配到的工具结果(matchedCalls)按当轮 tool_use 的原始定义顺序进行稳定排序后再组装输出,消除因并行执行异步竞态导致的 XML 文本乱序,恢复 Anthropic 严格前缀缓存命中。

背景与动机 / Context and motivation

  • 根因分析
    1. 在工具循环内部(同一轮次继续请求 AI 时),ToolExecutionManager.executeInvocations 会将结果重新按原始调用顺序(Invocation Order)排序放入内存上下文。
    2. 但在并行执行期间,各个异步工具协程是以“先完成先发射”的形式通过 collector.emit 输出并持久化到数据库的(完成顺序 / Completion Order)。
    3. 用户发送新消息后,历史从数据库重新加载并按 XML 文本顺序由 parseXmlToolResults 解析,导致下一轮请求发往 Claude 的 tool_result 块顺序与上一轮不一致。
    4. Anthropic Prompt Caching 依赖严格的前缀字节级匹配(Strict Prefix Match)。一旦同轮次内的 tool_result 块顺序发生变化,缓存前缀在此处断裂,导致从该位置往后的所有历史 token 产生高额的 Cache Miss。

改动范围 / Changes

  • ClaudeProvider.kt
    • PromptTurnKind.TOOL_RESULT 分支中,调用 consumeMatchingToolCalls 匹配结果后,根据 openToolUses 的原始调用顺序(useOrder)对 matchedCalls 进行排序,确保输出至 contentArraytool_result 块与上一轮定义的 tool_use 顺序严格一致。
    • ClaudeSerializedHistorybuildSerializedHistory 的可见性从 private 调整为 internal,以便单元测试覆盖。
  • ClaudeProviderHistoryTest.kt
    • 新增针对该场景的单元测试,模拟乱序记录的 XML 工具结果,断言其在序列化后稳定对齐到原始 tool_use 顺序。
  • 明确不包含:不改动其他模型提供商的组装逻辑;不修改底层的 XML 标签格式或消息存储规范。

兼容性与风险 / Compatibility and risks

  • 无数据库迁移、配置格式、网络协议或公开 API 变更。
  • 仅影响 ClaudeProvider 在存在多个 tool_result 时的 JSON 报文块序列化排列,不改变工具执行逻辑或结果文本内容。
  • 实际 Prompt Cache 命中率还取决于上游 API / 中转服务是否存在账号池轮换(账号切换会致使服务端缓存失效)。

关联 Issue / Related issue

Fixes #1159

验证方式 / Verification

检查或命令:
- GitHub Actions: Android Build / assembleDebug (Run 34505982738)
- GitHub Actions: Android Tests (Run 34511465652)
- app/src/test/java/com/ai/assistance/operit/api/chat/llmprovider/ClaudeProviderHistoryTest.kt
- git diff --check

结果:
- Android Build (assembleDebug):通过 (BUILD SUCCESSFUL)
- diff 检查:通过,无多余空白或无关文件
- 单元测试:新增用例验证在乱序 XML 输入下,发往 Claude 的消息体 tool_result 稳定保持调用顺序
- Android Tests:与此前 PR 一致,当前 upstream/dev 既有的两个测试文件(DeepseekProviderMediaRoleTest、XaiProviderReasoningTest)存在编译符号过期问题阻断 compileDebugUnitTestKotlin;本 PR 未做无关追修

证据 / Evidence

检查清单 / Checklist

  • 我已记录可复现验证和未运行项原因 / Reproducible verification and reasons for unrun checks are recorded
  • 日常开发 PR 的目标分支为 dev;如目标为 main,我已说明这是维护者发布同步 / The target branch is dev for regular work; if it is main, I explained why this is a maintainer-led release sync
  • 我已确认 Candidate checks 覆盖改动范围,并已区分上游测试源码失败 / Candidate checks covers the change scope and the upstream test-source failure is identified separately
  • 最终 diff 无无关、临时、生成、二进制或敏感内容 / Final diff has no unrelated, temporary, generated, binary, or secret content
  • 已提供对应的回归、UI、文档/字符串或兼容性证据 / Relevant regression, UI, docs/strings, or compatibility evidence is provided

… prompt cache

When tools execute in parallel, results are emitted to the database in completion order rather than invocation order. When rebuilding prompt history for subsequent turns, ClaudeProvider output tool_result blocks in XML text order. Because Anthropic Prompt Caching requires strict prefix matching, this ordering divergence between turns broke the prompt cache on every subsequent turn following parallel tool calls.

- Sort matched tool calls by openToolUses definition order before building Claude content blocks
- Expose ClaudeSerializedHistory and buildSerializedHistory for unit testing
- Add regression test in ClaudeProviderHistoryTest verifying parallel tool results are ordered by tool_use definition order

Fixes AAswordman#1159
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant