WIP: 补齐LLM流式工具调用聚合基础与Chat协议 #113

Closed
menghao wants to merge 1 commits from codex/ai-game-creator-app-mh into codex/ai-game-creator-app
3 changed files with 1315 additions and 21 deletions
@@ -133,6 +133,7 @@ fn stream_delta(delta_text: &str, accumulated_text: &str) -> platform_llm::LlmSt
accumulated_text: accumulated_text.to_string(),
delta_text: delta_text.to_string(),
finish_reason: None,
tool_call_deltas: Vec::new(),
}
}
@@ -0,0 +1,461 @@
# LLM 流式工具调用完整格式改造计划
日期:`2026-07-27`
状态:`任务一已完成,待审核`
## 1. 背景与问题
mentor 对本任务的说明是:当前 OpenAI Responses、流式返回和 Anthropic 只完成了较简单的版本,例如流式场景会丢弃 tool call。
当前 `platform-llm` 的统一流式增量 `LlmStreamDelta` 只承载:
- 累计文本 `accumulated_text`
- 当前文本增量 `delta_text`
- 结束原因 `finish_reason`
内部 `ParsedStreamEvent``stream_run` 的聚合状态也只处理文本、结束原因和 usage。流式请求结束时,`LlmRunResponse.tool_calls` 被固定设置为空数组,因此即使 Provider 的 SSE 已返回工具调用分片,上层仍无法取得完整 tool call。
三个协议的具体缺口如下:
1. OpenAI Chat 流式分支没有聚合 `choices[].delta.tool_calls[]`
2. OpenAI Responses 流式分支主要处理 `response.output_text.delta`,没有完整处理 function call output item 生命周期。
3. Anthropic 流式分支主要处理 `text_delta`,没有处理 `tool_use``input_json_delta`;请求侧当前也拒绝 function tools。
4. 现有流完成与尾错保留逻辑以“已经得到文本”为主要成功条件,无法正确接受只有 tool call、没有文本的合法响应。
## 2. 改造目标
本次改造完成后,`platform-llm` 应满足:
1. 文本流行为与现有调用方保持兼容。
2. 流式 tool call 分片不会被丢弃。
3. callback 能收到协议无关的标准化 tool call 增量。
4. 多个并行或交错返回的 tool call 可以独立、确定性地聚合。
5. 流结束时生成完整的 `LlmRunResponse.tool_calls`
6. 只有 tool call、没有文本的流式响应也能成功返回。
7. OpenAI Chat、OpenAI Responses 和 Anthropic 最终输出相同的统一 `LlmToolCall`
8. Provider 特有 SSE 结构只留在 `platform-llm`,不泄漏到业务 Runtime。
9. 流式与非流式对相同 Provider 响应形成一致的最终文本、tool calls、结束状态和 usage 语义。
## 3. 本次范围
### 3.1 纳入范围
- 扩展统一流式增量和内部解析事件。
- 建立协议无关的 tool call 聚合状态机。
- 补齐 OpenAI Chat 流式 tool calls。
- 补齐 OpenAI Responses function call 流式事件。
- 补齐 Anthropic tools 请求映射、非流式 `tool_use` 和流式 `tool_use`
- 调整流完成、空响应、尾错保留和 usage 汇总逻辑。
- 审计并兼容当前所有 `stream_run` callback 调用方。
- 增加三种协议的确定性 mock SSE 测试和下游回归。
- 更新 `platform-llm` README、AI 游戏创作技术方案和必要的共享项目记忆。
### 3.2 暂不纳入范围
- Markdown 或其它前端富文本渲染。
- 新增 LLM Provider 或模型选择功能。
- 音频、视频、任意文件等新的多模态类型。
- Realtime API。
- 改造 Agent 工具实际执行器、权限、确认、持久化或恢复语义。
- 静默扩展现有 `/api/llm/*` 对外 HTTP/SSE 契约。
- 与流式 tool call 无关的全量上游字段照搬。
- 未经单独评审的 reasoning 原文公开或持久化。
Anthropic `tool_result` 请求回传属于完整工具闭环。如果任务一至任务三实施时现有调用链已经需要它,则与 `tool_use` 一并补齐;如果当前没有调用方,本计划要求先冻结统一消息契约和测试,但不得在文档中宣称真实工具回传闭环已经完成。
## 4. 设计原则
1. **统一聚合,不复制三套状态机**:协议解析器只把原始事件映射成标准化内部事件,完整 tool call 由一个聚合器生成。
2. **公共改动尽量增量兼容**:保留现有文本字段和 `LlmRunResponse.tool_calls`,通过新增字段承载工具分片。
3. **身份冲突失败关闭**:无法唯一定位工具调用分片时返回协议错误,不把参数拼到错误调用。
4. **确定性顺序**:最终 tool calls 按 Provider 声明的顺序或首次出现顺序输出。
5. **不提前解释业务参数**:平台层负责完整拼接 arguments;具体工具 schema 和业务合法性仍由上层校验。
6. **工具调用也是有效结果**:文本为空但存在完整 tool calls 时不得返回 `EmptyResponse`
7. **不以真实 Provider 代替确定性测试**:协议解析先用冻结 fixtures 覆盖,再视配置情况补真实网关 smoke。
8. **保持现有安全边界**:不得把 reasoning、密钥、原始请求头、私有正文或绝对路径新增到公共事件和日志。
## 5. 公共结构改造
### 5.1 扩展 `LlmStreamDelta`
保留现有字段和语义:
| 字段 | 处理 |
| --- | --- |
| `accumulated_text` | 保留,继续表示当前累计可见文本 |
| `delta_text` | 保留,继续表示本次可见文本增量 |
| `finish_reason` | 保留,继续表示当前已知结束原因 |
| `tool_call_deltas` | 新增,表示本次事件携带的标准化工具调用分片 |
新增标准化 tool call delta 类型,至少承载:
| 字段 | 含义 |
| --- | --- |
| `index` | 当前响应内工具调用的稳定顺序 |
| `id` | 本次分片携带的工具调用 ID,可选 |
| `name` | 本次分片携带的函数名,可选 |
| `arguments_delta` | 本次新增的参数字符串,可为空 |
| `is_done` | 是否收到该工具调用的协议级完成信号 |
流式早期可能尚未取得 ID、函数名或完整 arguments,因此不能直接用所有字段均必填的最终 `LlmToolCall` 替代 delta 类型。
### 5.2 保持最终响应稳定
`LlmRunResponse.tool_calls: Vec<LlmToolCall>` 保持现有类型和最终语义。主要下游不需要迁移最终响应类型,只需要 `platform-llm` 在流结束时真正填充它。
### 5.3 callback 新语义
改造后 callback 可能在“没有文本、只有 tool call 分片”时触发。所有调用方必须显式处理该变化,不能假设每次 callback 都有非空 `delta_text` 或变化后的 `accumulated_text`
## 6. 内部事件与聚合状态机
### 6.1 扩展 `ParsedStreamEvent`
在现有文本、finish reason、usage 和 terminal 字段之外,新增标准化 `tool_call_deltas`。三个协议解析分支只负责输出标准化事件,不直接持有各自独立的最终 tool call 数组。
### 6.2 聚合状态
一次 `stream_run` 内统一维护:
- 累计文本
- 当前结束原因
- 当前 usage
- 按工具调用槽位保存的 pending tool calls
- 工具调用首次出现顺序
- 各工具调用是否完成
- 整个响应是否终止
每个 pending tool call 至少保存:
- 标准化 index
- 当前已知 ID
- 当前已知函数名
- 已累计 arguments
- 是否收到完成信号
- 仅供内部匹配的 Provider 定位信息
### 6.3 分片匹配规则
按以下优先级定位同一工具调用:
1. Provider 提供的 `index``output_index` 或 content block index。
2. 已存在的 tool call ID、item ID 或 call ID。
3. 只有一个 pending call 时允许唯一回退。
4. 无法唯一定位或身份发生冲突时,返回 `Deserialize` 类协议错误。
### 6.4 arguments 聚合规则
1. 按 SSE 到达顺序追加 arguments delta。
2. 空 delta 不产生内容变化。
3. done 事件携带完整 arguments 时:
- 与累计值一致则确认完成;
- 累计值为空则采用 done 值;
- 两者冲突则失败关闭。
4. 多个工具调用必须拥有独立缓冲区。
5. 平台层保留最终 arguments 字符串,不替代上层工具 schema 校验。
### 6.5 流结束汇总
收到协议终止事件或正常 EOF 后:
1. 按首次出现顺序遍历 pending tool calls。
2. 校验每个调用具有非空 ID 和函数名。
3. 生成完整 `LlmToolCall`
4. 文本为空但 `tool_calls` 非空时返回成功。
5. 文本和 `tool_calls` 都为空时才返回 `EmptyResponse`
6. tool call 身份不完整时不得用 finish reason 掩盖错误。
## 7. OpenAI Chat 流式改造
### 7.1 解析范围
在现有 `choices[0].delta.content`、finish reason 和 usage 之外,解析:
- `choices[0].delta.tool_calls[].index`
- `tool_calls[].id`
- `tool_calls[].function.name`
- `tool_calls[].function.arguments`
### 7.2 必须覆盖的情况
- ID、name、arguments 一次完整返回。
- ID 只在第一帧出现。
- name 和 arguments 位于不同帧。
- arguments 被拆成多个字符串片段。
- 两个或更多 tool calls 交错返回。
- 同一帧同时包含文本和 tool call。
- `finish_reason = tool_calls` 且没有文本。
- usage-only 尾包。
- `[DONE]` 终止。
- 空或 `null` choices 心跳包保持现有兼容行为。
- SSE chunk 和 UTF-8 字符边界被任意拆分。
### 7.3 阶段验收
相同 mock 响应分别走非流式和流式后,文本、tool calls、顺序、arguments 和 finish reason 应一致。
## 8. OpenAI Responses 流式改造
### 8.1 事件范围
至少处理:
- `response.output_text.delta`
- `response.output_item.added`
- `response.function_call_arguments.delta`
- `response.function_call_arguments.done`
- `response.output_item.done`
- `response.completed`
- `response.failed`
- `error`
实现前须通过 OpenAI 官方开发文档和 API schema 冻结当前事件字段,兼容网关变体只在已有 fixture 或真实证据支持时加入,不能凭猜测扩展。
### 8.2 聚合规则
1. `response.output_item.added`:当 item 为 `function_call` 时,按 `output_index` 创建槽位,记录 item ID、call ID、name 和可能存在的初始 arguments。
2. `response.function_call_arguments.delta`:按 output index 或 item ID 找到槽位并追加 delta。
3. `response.function_call_arguments.done`:标记参数完成;如果携带完整 arguments,执行一致性核对。
4. `response.output_item.done`:补齐最终身份、name 和 arguments,并标记对应调用完成。
5. `response.completed`:提取最终 status、usage 和响应身份,标记整个流终止。
6. `response.failed``error`:映射成稳定的上游错误,不返回部分未完成 tool call。
### 8.3 兼容变体
聚合器允许合法的“只在 `output_item.done` 返回完整对象”变体;如果同一槽位的 index、item ID、call ID 或函数名冲突,则失败关闭。
## 9. Anthropic 改造
### 9.1 请求与非流式前置补齐
为了让真实 Anthropic 流式 tool call 可达,需要同步:
- 将统一 `LlmFunctionTool` 映射到 Anthropic `tools`
- 映射 Anthropic `tool_choice`
- 移除当前对 Anthropic function tools 的本地拒绝。
- 保留与本任务无关的 web search 拒绝。
- 非流式响应识别 `text``tool_use` content block。
### 9.2 流式事件范围
- `message_start`
- `content_block_start`
- `content_block_delta`
- `content_block_stop`
- `message_delta`
- `message_stop`
- `error`
### 9.3 tool use 聚合
1. `content_block_start`:当 block 类型为 `tool_use` 时,按 content block index 创建槽位,记录 tool use ID、name 和可能存在的初始 input。
2. `content_block_delta`:当 delta 类型为 `input_json_delta` 时,读取并追加 `partial_json`,同时产生标准化 tool call delta。
3. `content_block_stop`:标记当前工具调用完成,但不终止整个 message。
4. `message_delta`:保存 stop reason 并合并 output token usage。
5. `message_stop`:终止整个流并汇总文本与 tool calls。
文本 block 继续按现有 `text_delta` 规则处理。非流式与流式对相同内容应输出一致的文本和统一 `LlmToolCall`
## 10. 流完成、尾错与 usage
需要统一调整:
1. `EmptyResponse` 判断改为“文本和工具调用都为空”。
2. tail error 保留逻辑同时考虑已完成文本和已完成 tool calls。
3. 已收到 finish reason 且存在完整 tool call 时,允许按现有容错类别保留完成结果。
4. pending tool call 缺少 ID 或 name 时不得保留为成功结果。
5. terminal 事件后忽略多余尾包,保持防重复语义。
6. usage 可来自独立尾包、Responses completed 或 Anthropic message delta。
7. 每个标准化事件最多触发一次 callback:有文本、有 tool call 分片,或现有协议要求传递 finish reason 时触发;纯 usage 更新默认不触发 UI callback。
## 11. 调用方兼容审计
### 11.1 通用 `/api/llm/*` SSE
- 第一阶段保持现有 HTTP SSE 文本契约不变。
- tool-only callback 不发送空文本事件。
- 是否向外部 complete 事件增加 tool calls 必须先评审 shared contract,不在本任务中静默扩展。
### 11.2 Creation Agent JSON 流
- 只在累计文本发生变化时继续提取部分 JSON。
- tool-only callback 不重复触发 `on_reply_update`
### 11.3 RPG 和 runtime chat
- 只有文本变化时刷新 UI。
- tool-only 增量不得清空或重复覆盖当前文本。
### 11.4 AI Game Creator Shell
- 保持 `accumulated_text``finish_reason` 现有语义。
- tool-only callback 不产生空消息。
- 当前非流式 native tool planning、Provider lifecycle、handoff、持久化和“未知结果不得原样重放”边界保持不变。
### 11.5 Rust 公共类型兼容
新增公共 struct 字段会影响通过 struct literal 构造它的代码。实施前必须全仓搜索 `LlmStreamDelta {``ParsedStreamEvent {` 和相关 fixtures,迁移全部仓库内构造点。
不为避免 struct literal 迁移而新建第二套平行流式 API;如发现仓库外必须保持的公开兼容承诺,再单独评审兼容入口。
## 12. 测试矩阵
### 12.1 聚合器纯单测
- 单工具分片。
- 多工具交错。
- ID 或 name 晚到。
- arguments 多段拼接。
- done 完整值与累计值一致。
- done 完整值冲突。
- 缺 ID 或缺 name。
- tool-only 响应。
- 文本与工具混合。
- 确定性输出顺序。
### 12.2 OpenAI Chat SSE
- 完整 tool call。
- 分段 arguments。
- 多 tool calls。
- finish-only。
- usage-only。
- `[DONE]`
- chunk 与 UTF-8 边界。
- malformed 尾包。
### 12.3 OpenAI Responses SSE
- item added → arguments delta → done → completed。
- 只有 output item done。
- 多 output items。
- 文本和 function call 混合。
- completed usage。
- failed 和 error。
### 12.4 Anthropic
- 非流式 text + tool use。
- 流式纯文本。
- 单个 tool use。
- 多个 content blocks。
- text + tool use。
- 多段 `partial_json`
- message delta usage 和 stop reason。
- message stop。
- error。
### 12.5 下游回归
- `platform-llm` 全量测试。
- `api-server` LLM、Creation Agent 和 runtime chat 定向测试。
- AI Game Creator Shell 流式聊天和 native tool planning 定向测试。
- 现有纯文本流输出保持不变。
- 非流式工具计划行为保持不变。
## 13. 实施阶段与任务拆分
所有任务严格串行执行,不并行修改 `platform-llm/src/lib.rs`
### 任务一:公共结构、聚合器与 OpenAI Chat
范围:
1. 扩展 `LlmStreamDelta` 和内部解析事件。
2. 实现协议无关 tool call 聚合器。
3. 调整基础流完成、空响应和尾错判断。
4. 补齐 OpenAI Chat 流式 tool calls。
5. 完成聚合器和 Chat 测试。
非目标:不处理 Responses 和 Anthropic。
建议提交标题:`补齐LLM流式工具调用聚合基础与Chat协议`
### 任务二:OpenAI Responses
前置:任务一已经审核并形成稳定提交。
范围:
1. 补齐 Responses function call 流式事件。
2. 对齐 Responses 非流式和流式最终结果。
3. 完成 Responses 测试矩阵。
非目标:不处理 Anthropic,不改调用方公开契约。
建议提交标题:`补齐OpenAI Responses流式工具调用`
### 任务三:Anthropic
前置:任务二已经审核并形成稳定提交。
范围:
1. 补齐 tools 和 tool choice 请求映射。
2. 补齐非流式 `tool_use`
3. 补齐流式 content block tool use。
4. 完成 Anthropic 测试矩阵。
建议提交标题:`补齐Anthropic工具调用与流式协议`
### 任务四:调用方兼容、全量验证与文档
前置:三种协议实现均已审核。
范围:
1. 审计全部 `stream_run` callback。
2. 避免 tool-only callback 产生空 UI 或 SSE 更新。
3. 回归 api-server 和 AI Game Creator Shell。
4. 执行完整验证矩阵。
5. 更新 README、技术方案和共享项目记忆。
建议提交标题:`收口LLM完整流式格式兼容与验证`
## 14. 验证顺序
各阶段按修改范围执行,最终阶段至少运行:
```bash
cargo test -p platform-llm --manifest-path server-rs/Cargo.toml
cargo check -p platform-llm --manifest-path server-rs/Cargo.toml
# 根据实际受影响模块追加 api-server 与 AI Game Creator Shell 定向测试
npm run check:encoding
git diff --check
```
如果改动实际进入 AI Game Creator Runtime 的 Provider native tool planning 路径,追加当前 AI 游戏创作技术方案规定的 native tool planning 定向回归。真实外部 Provider smoke 只作为补充,不替代确定性协议测试;没有真实配置时必须如实记录未执行。
## 15. 阶段交接规则
每个任务完成后更新本节,不依赖复制聊天记录。交接必须包含:
- 完成范围
- 关键设计决定
- 修改文件
- 已执行验证及结果
- 未执行验证及原因
- 当前提交 hash(提交后填写)
- 已知风险
- 下一任务入口
### 当前交接
- 已完成范围:任务一。已新增公共 `LlmToolCallDelta` 并扩展 `LlmStreamDelta.tool_call_deltas`;内部 `ParsedStreamEvent` 已承载协议无关工具分片;统一聚合器已支持按 index、ID 和唯一 pending 槽位匹配,支持 ID / name 晚到、arguments 分段追加、完成值一致性校验、身份冲突失败关闭和按 Provider index 确定性输出;OpenAI Chat SSE 已解析 `delta.tool_calls`,tool-only 流可成功生成最终 `LlmRunResponse.tool_calls`;空响应和尾错保留已同时考虑完整 tool calls。
- 关键设计决定:Provider 原始分片先映射为内部 `ParsedToolCallDelta`,聚合器解析稳定 index 后再向 callback 输出公共 `LlmToolCallDelta`Chat 的 `finish_reason``[DONE]` 负责把尚未完成的 pending calls 标记为 done;正常 EOF 可汇总身份完整的调用,tail error 只有在 finish reason 已存在且文本或全部工具调用已完整时才保留结果;最终调用缺少 ID / name、同一 index 身份冲突、同一 ID 指向不同 index、arguments 完成值冲突或多 pending 下无法定位分片时统一返回 `Deserialize`
- 修改文件:`server-rs/crates/platform-llm/src/lib.rs``apps/ai-game-creator-shell/src-tauri/src/agent.rs`(仅迁移测试中的 `LlmStreamDelta` struct literal)、本计划文档。
- 已执行验证:`cargo test -p platform-llm --manifest-path server-rs/Cargo.toml`50 passed`cargo check -p platform-llm --manifest-path server-rs/Cargo.toml`,通过;`cargo check -p api-server --manifest-path server-rs/Cargo.toml`,通过;`cargo check --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`,通过,仅有既有 unused / dead-code warnings`cargo fmt --manifest-path server-rs/Cargo.toml --all -- --check`,通过;`npm run check:encoding`5237 files passed`git diff --check`,通过。
- 未执行验证:未运行真实外部 Provider smoke;任务一使用确定性 mock SSE 覆盖协议行为,且本次未读取 `.env` / `.env.local` 或真实密钥。未运行 Responses / Anthropic 新协议测试,因为它们明确属于任务二、任务三。
- 当前提交 hash:本阶段代码与本交接同提交,以当前分支提交历史为准。
- 已知风险:OpenAI Responses 和 Anthropic 流式工具调用仍未实现;公共 callback 现在会收到 tool-only 更新,通用 `/api/llm/*`、Creation Agent、RPG/runtime chat 和 AI Game Creator Shell 的完整兼容收口仍属于任务四,在任务四前可能出现空文本回调或重复 UI 更新;本阶段没有扩展现有对外 HTTP/SSE complete 契约。
- 已知工作区修改:`.env``.env.local` 为任务开始前已有用户修改,本任务未读取、覆盖或提交。
- 下一步:任务一形成稳定提交后,按任务二范围补齐 OpenAI Responses,不提前处理 Anthropic。
## 16. 新任务提示词
任务一建议使用:
> 阅读 `AGENTS.md` 和 `docs/project-memory/plans/【计划】LLM流式工具调用完整格式改造-2026-07-27.md`,执行计划中的任务一:扩展公共流式结构、实现协议无关 tool call 聚合器、补齐 OpenAI Chat 流式 tool calls,并完成对应测试。不要处理 OpenAI Responses 和 Anthropic,不做无关重构。完成后更新计划文档的阶段交接,但先不要提交,等待审核。
任务二至任务四沿用该格式,只替换阶段范围,并要求先核对上一阶段提交和交接记录。
File diff suppressed because it is too large Load Diff