# platform-llm 平台适配 crate 更新:`2026-09-10` ## 1. crate 职责 `platform-llm` 是 Rust 工作区里的大模型平台适配 crate,当前已经落地以下能力: 1. 统一 Ark / DashScope / Anthropic / 其他兼容网关的文本模型配置结构 2. 统一 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 的文本 / 原生 function tool 请求、非流式响应与 SSE 流式解析 3. 统一超时、连接失败、上游错误、空响应与重试策略 4. 为后续 `module-ai`、`module-story`、`module-npc`、`module-custom-world` 提供可直接复用的基础 client ## 2. 当前能力边界 当前实现只覆盖“文本 run”主链,不提前混入媒体生成和业务编排: 1. 对外抽象固定为 `LlmRunRequest` / `LlmRunResponse`,不再保留旧 `LlmTextRequest` / `LlmTextResponse` 类型。 2. `OpenAiChat`、`OpenAiResponses` 与 `Anthropic` 三类 API kind 都支持 JSON 请求、非流式响应和 SSE 流式响应;默认 API kind 仍为 `OpenAiResponses`。 3. 三类协议都使用统一的 `function_tools` / `tool_choice` 输入和 `LlmRunResponse.tool_calls` 输出。Anthropic 请求使用顶层 `tools[].input_schema` 与对象形态 `tool_choice`;Anthropic URL 默认在 base URL 后拼 `/v1/messages`,如果 base URL 已以 `/v1` 结尾则只拼 `/messages`。 4. Anthropic 当前仍不支持 `web_search`、图片内容和纯 system 消息;至少需要一条非 system 文本消息。角色动画、图片、视频、资产轮询仍留在其他平台适配和业务模块任务里。 5. 流式 `on_delta` 发送正文增量、可选的独立 reasoning 增量与完成原因;reasoning 只有在 `LlmRunRequest.capture_reasoning=true` 时才累计,默认关闭。Responses 的 reasoning summary 按 `item_id + summary_index` 分段累计,单段 `.done` 只校正对应段,不覆盖其它 item;Anthropic 的 `thinking_delta` 同样进入独立 reasoning 通道。分段快照即使改写了原内容也会通知调用方刷新累计 reasoning。工具调用增量在 crate 内按 slot 聚合,完整调用只从最终 `LlmRunResponse.tool_calls` 读取。reasoning 不进入 `delta_text`、`accumulated_text`、正式 assistant message 或工具参数;上下文管理、后台执行和业务状态不写回本 crate。 6. 支持按 provider 打标签,但不把业务 prompt、SSE 转发和模块状态写回本 crate。 7. `DashScope` 当前只通过“调用方显式提供兼容文本网关 base url”的方式接入,不复用图像 API。 8. 角色动画、图片、视频、资产轮询仍留在后续 `platform-llm` / `platform-oss` / 业务模块任务里另行实现。 ## 3. 三协议工具支持矩阵 | API kind | 请求工具形态 | `tool_choice` | 非流式工具响应 | 流式工具聚合 | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `OpenAiChat` | `tools[].type=function`,函数内为 `name` / `description` / `parameters` / `strict` | `"auto"` / `"required"` | `choices[0].message.tool_calls` | `delta.tool_calls[].index`;首片提供 id/name,后续拼接 arguments | | `OpenAiResponses` | `tools[].type=function`,函数内为 `name` / `description` / `parameters` / `strict` | `"auto"` / `"required"` | `output[].type=function_call` | `output_index`;`output_item.added` 提供身份,`function_call_arguments.delta` 拼接,`.done` 覆盖完整参数 | | `Anthropic` | 顶层 `tools[]` 为 `name` / `description` / `input_schema`,没有 `function` 包装层;只有 endpoint/model 配置显式声明支持且 schema / 请求复杂度满足 Anthropic 当前边界时才发送 `strict: true`。strict 传输 schema 会剥离不支持的约束,调用方原 schema 保持不变;最后一项带 ephemeral cache breakpoint | `{ "type": "auto" }` / `{ "type": "any" }`;`Required` 映射为 `any` | `content[].type=tool_use`,`input` 序列化为 `arguments` | content block `index`;`content_block_start` 提供身份,`input_json_delta` 拼接参数;`message_start/message_delta` 合并 cache/input/output usage | Responses 如果只发送 `response.completed` 或 `response.incomplete`,解析器会从其中的 `response.output[]` 恢复 `function_call`;恢复时使用 output 数组下标作为 slot。`response.incomplete` 表示上游没有完成本轮生成:其中的工具调用即使参数是完整 JSON 也返回 `Deserialize`,纯正文则保留为可用的降级结果。三种协议的并行工具调用只在平台层做 slot 聚合,不代表工具会在平台层并发执行。 需要原生工具续轮时,调用 `LlmRunRequest::with_responses_input(Vec)`,由调用方提供包含当前 system 指令的完整 input,替代 `messages`。该入口固定使用 Responses,并发送 `store=false` 和 `include=["reasoning.encrypted_content"]`。`LlmRunResponse.responses_output` 原样保存非流式 output 数组或流式终态事件的 response.output 数组,包含 reasoning、function_call 和 message;调用方将这些项按顺序追加到历史,并在对应 function_call 后加入同 call_id 的 function_call_output。更换阶段指令时替换 input 中的 system 消息,不依赖 previous_response_id。普通 messages 入口保留原请求参数;其他协议的 responses_output 为空。neutral provider adapter 无法无损表达原生历史,拒绝该输入,使用此入口必须直接调用 LlmClient。 ## 4. 流式与参数契约 1. `LlmRunRequest.max_output_tokens` 是协议中立的生成预算,包含可见输出与 Provider 可能使用的隐藏 reasoning token,不包含输入 token,也不保证可见正文长度。Responses 映射为 `max_output_tokens`,Anthropic 映射为 `max_tokens`;Chat 由 `LlmConfig.openai_chat_token_budget_field` 显式映射为当前 `max_completion_tokens` 或兼容网关旧字段 `max_tokens`,每次只发送一个。默认保留 legacy,已验证支持新字段的 endpoint 必须显式 opt-in;禁止按模型名猜测或收到 `400` 后自动重放。 2. `LlmStreamDelta` 只包含 `accumulated_text`、`delta_text` 和 `finish_reason`,工具调用不会进入 `on_delta`;纯工具响应允许 `text` 为空。 3. 工具片段按协议索引聚合:Chat 使用 `delta.tool_calls[].index`,Responses 使用 `output_index`,Anthropic 使用 content block `index`。Responses 的 `.done`、`response.completed` 和 `response.incomplete` 中的完整 arguments 是权威值,可以覆盖之前的分片拼接。 4. 流结束固化工具调用时,缺少 id 或函数名返回 `Deserialize`;空参数默认保存为 `{}`;非空参数必须能反序列化为完整 JSON,截断或半截 JSON 不会交给业务层。这里是 JSON 语法完整性检查,不是针对 `parameters` 的 JSON Schema 业务校验。 5. 非流式工具调用采用不同的参数边界:缺失或空白 `arguments` 统一归一为 `{}`;Chat / Responses 的非空畸形 `arguments` 不在平台层做 JSON 校验、修复或静默丢弃,而是保留参数内容(仅按统一归一策略去除首尾空白),连同 call id 和函数名交给调用方的 repair 循环。Anthropic `tool_use.input` 缺失时同样按 `{}` 归一;调用方不能把非流式参数自动假定为统一 schema 校验通过。 ## 5. 错误边界 1. `Deserialize`:上游 JSON、SSE 或 UTF-8 无法解析,Chat 非流式缺少 `choices[0]`,流式工具调用缺少 id / name,或流式工具参数不是完整 JSON。 2. `StreamUnavailable`:仅用于流式响应已声明 `tool_use` / `tool_calls`,但一个工具 slot 都没有聚合出来的协议兼容失败;调用方可以据此回退一次非流式请求,不能把已收到的解说文本当成最终回复。 3. `EmptyResponse`:最终文本为空且工具调用也为空。只有文本为空但存在有效工具调用时,响应才是合法的纯工具响应。 4. 流尾部出现 `Timeout`、`Connectivity`、`Transport` 或 `Deserialize` 时,只有已形成非空文本或至少一个工具调用、观察到协议完成信号、已有完成原因且工具参数完整的响应才会保留;纯工具响应即使 `text` 为空也可以保留。工具参数半截或既没有文本也没有工具调用时继续返回错误。 ## 6. 核心导出 当前对外导出以下公共类型: 1. `LlmProvider` 2. `LlmConfig` 3. `OpenAiChatTokenBudgetField` 4. `LlmMessageRole` 5. `LlmMessage` 6. `LlmRunRequest` 7. `LlmApiKind` 8. `LlmStreamDelta` 9. `LlmFunctionTool` 10. `LlmToolChoice` 11. `LlmToolCall` 12. `LlmRunResponse` 13. `LlmTokenUsage` 14. `LlmClient` 15. `LlmError` ## 7. 设计文档 ## 当前文档入口 当前长期工程口径已融合到: 1. [../../../docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md](../../../docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md) 2. [../../../docs/【玩法创作】平台入口与玩法链路-2026-05-15.md](../../../docs/【玩法创作】平台入口与玩法链路-2026-05-15.md) 3. [../../../docs/【开发运维】本地开发验证与生产运维-2026-05-15.md](../../../docs/【开发运维】本地开发验证与生产运维-2026-05-15.md) 旧阶段设计文档不再作为实现依据。 ## 8. 边界约束 1. `platform-llm` 只承接模型平台适配,不承接业务模块状态真相与业务规则。 2. 业务模块只能依赖这里的统一 client / DTO / 错误模型,不能再把上游请求细节散落回各 crate。 3. `api-server` 后续如果需要做 REST/SSE façade,只允许在协议层调用 `platform-llm`,不能复制一份私有实现。 ## 9. 验收证据边界 1. `cargo test -p platform-llm` 的确定性用例把 checked-in SSE fixture 交给 parser,验证归一后的文本、工具调用、slot 聚合、Responses 仅有 completed / incomplete 终态事件时的恢复、参数 JSON 完整性和错误边界;同时包含 `tests/live_stream_tool_calls.rs` 中不依赖外部 Provider 的 `parse_api_kind` 本地解析测试。fixture 可以来源于真实端点抓包,但测试不保存原始 SSE,也不逐事件与端点报文比较,因此不能证明抓包转录无偏差。 2. `tests/live_stream_tool_calls.rs` 同时包含默认执行的 `parse_api_kind` 确定性测试,以及 1 个默认 `#[ignore]` 的真实端点工具调用 smoke。`PLATFORM_LLM_LIVE_API_KIND` 支持 `openai_responses`、`openai_chat` 和 `anthropic`,未设置或空白时默认 `openai_responses`,未知非空值会直接使验收失败。真实 smoke 只验证最终归一结果中的工具名、id 和完整参数 JSON;文本增量字符数仅用于打印观测,工具调用不进入 `on_delta`,也没有原始 SSE 录制或逐事件对比能力。 3. 因此验收应分别称为“固定 SSE fixture parser 覆盖及本地解析单元测试”和“真实端点归一工具调用 smoke”,不能把后者描述为原始 SSE fidelity 或转录一致性证明。