# `platform-llm` 文本网关首版设计(2026-04-21) ## 1. 背景 `server-rs/crates/platform-llm/` 在 `2026-04-20` 只完成了目录占位,但当前仓库里已经存在一条稳定的 Node 侧文本模型主链: 1. `server-node/src/services/llmClient.ts` 2. `server-node/src/modules/ai/*` 3. `server-node/src/services/storyService.ts` 4. `server-node/src/services/questService.ts` 5. `server-node/src/services/runtimeItemService.ts` 这些调用点已经依赖一套隐含约束: 1. 使用 OpenAI 兼容的 `/chat/completions` 2. 统一 Bearer 鉴权 3. 同时支持非流式 JSON 响应与 SSE 流式增量 4. 要求有超时、连接失败、上游错误和空响应兜底 如果 Rust 侧继续只保留 README 占位,后续 `api-server`、`module-ai`、`module-story`、`module-npc` 在落地时又会各自复制一份私有上游 client,重新造成平台层分叉。 因此本次先把 `platform-llm` 收口成一个真实可编译、可测试、可复用的 Rust crate,冻结文本主链基础设施。 ## 2. 本次落地范围 ### 2.1 本次明确实现 1. `LlmProvider`:冻结 provider 来源标签,首版包含 `ark`、`dash_scope`、`openai_compatible` 2. `LlmConfig`:统一 base url、api key、model、timeout、retry 配置 3. `LlmMessageRole`、`LlmMessage`、`LlmTextRequest`:统一请求 DTO 4. `LlmClient::request_text(...)`:统一非流式文本调用 5. `LlmClient::stream_text(...)`:统一流式 SSE 文本调用 6. `LlmTextResponse`、`LlmStreamDelta`、`LlmTokenUsage`:统一响应 DTO 7. `LlmError`:统一配置错误、请求错误、超时、连接失败、上游错误、反序列化错误、空响应错误 8. 基础重试策略:对 `408`、`429`、`5xx`、超时、连接失败重试 ### 2.2 本次明确不做 1. 不在 `platform-llm` 内承接业务 prompt 组织 2. 不在 `platform-llm` 内承接模块级状态写回 3. 不在 `platform-llm` 内做 HTTP Route/SSE façade 4. 不提前把图片、视频、异步任务轮询混进同一个 crate 5. 不声称已经打通 DashScope 图像 API;当前首版只做文本网关 ## 3. 当前边界口径 ### 3.1 文本协议边界 首版只冻结 **OpenAI 兼容 chat completion**: 1. 请求路径固定为 `base_url + /chat/completions` 2. Bearer Token 由 `Authorization: Bearer ` 注入 3. 非流式返回解析 `choices[0].message.content` 4. 流式返回解析 `choices[0].delta.content` 5. `content` 若返回数组文本片段,也统一拼成单字符串 ### 3.2 Provider 边界 1. `Ark`:当前仓库已有真实默认 base url,可直接作为 Rust 首版默认值 2. `DashScope`:当前只保留 provider 标签,不在 crate 内硬编码其文本兼容入口 3. `OpenAiCompatible`:用于其他兼容网关 这里故意不把 DashScope 文本 base url 写死,是因为当前仓库的真实 Node 主链并没有用 DashScope 跑文本 `/chat/completions`,而是主要用于图像任务;Rust 首版不应在没有仓库事实对齐的前提下硬塞一个未经验证的默认路径。 ## 4. 对外 API 设计 ### 4.1 `LlmConfig` 字段: 1. `provider` 2. `base_url` 3. `api_key` 4. `model` 5. `request_timeout_ms` 6. `max_retries` 7. `retry_backoff_ms` 约束: 1. `base_url`、`api_key`、`model` 不允许为空 2. `request_timeout_ms` 必须大于 `0` 3. `max_retries` 表示“首轮之外还允许重试多少次” ### 4.2 `LlmTextRequest` 字段: 1. `model: Option` 2. `messages: Vec` 3. `max_tokens: Option` 约束: 1. `messages` 不能为空 2. 每条 `message.content` 不能为空字符串 3. `model` 如果传入,则 trim 后不能为空 ### 4.3 `LlmTextResponse` 字段: 1. `provider` 2. `model` 3. `content` 4. `finish_reason` 5. `response_id` 6. `usage` 设计目的: 1. 上层只拿统一文本结果,不再接触 `choices`、`delta`、`message` 等上游细节 2. 后续 `api-server` 可以直接把这些字段映射到自己的 HTTP / SSE contract ## 5. 错误与重试策略 ### 5.1 错误分层 `LlmError` 首版固定为: 1. `InvalidConfig` 2. `InvalidRequest` 3. `Timeout` 4. `Connectivity` 5. `Upstream` 6. `StreamUnavailable` 7. `EmptyResponse` 8. `Transport` 9. `Deserialize` ### 5.2 重试规则 允许重试: 1. 请求超时 2. 连接失败 3. `408` 4. `429` 5. `5xx` 不重试: 1. 配置错误 2. 请求体无效 3. 上游返回 `4xx` 非限流类错误 4. 已成功开始返回流之后的解析错误 ### 5.3 Backoff 规则 首版采用线性 backoff: 1. 第 1 次重试等待 `retry_backoff_ms` 2. 第 2 次重试等待 `retry_backoff_ms * 2` 3. 依此类推 原因: 1. 先保持实现简单 2. 足以覆盖当前仓库文本上游的偶发抖动 3. 真正需要指数退避时,再在平台层单点升级即可 ## 6. 与 Node 现状对齐 Rust 首版有意对齐 `server-node/src/services/llmClient.ts` 的事实边界: 1. 同样走 `/chat/completions` 2. 同样区分非流式与流式 3. 同样在空文本时直接报错 4. 同样把上游 JSON 错误体里的 `error.message` / `message` 提取出来 5. 同样把重试、超时、连接失败收口在一个平台层里 但 Rust 版这次额外收紧两点: 1. 不混入 Express Request/Response 转发逻辑 2. 不把业务 prompt 参数与上游 client 绑定在一起 这样后续 `api-server` 和 `module-ai` 都只能依赖一套稳定基础设施,而不是复制旧 Node 的“传 HTTP 对象进去直接转发”的实现方式。 ## 7. 当前测试覆盖 首版要求至少覆盖: 1. 配置校验 2. URL 归一化 3. SSE 事件解析 4. 非流式成功响应解析 5. `500 -> retry -> 200` 的重试闭环 6. 流式累计文本拼接 ## 8. 后续衔接 ### 8.1 `api-server` 后续 `api-server` 应该: 1. 在自身配置层解析环境变量 2. 组装 `LlmConfig` 3. 注入 `LlmClient` 4. 在 handler / application façade 中调用 `request_text` 或 `stream_text` ### 8.2 `module-ai` 后续 `module-ai` 应该: 1. 只负责 prompt 组织、阶段状态和结果引用 2. 不直接依赖 `reqwest` 3. 不再自己解析 SSE 增量 4. 统一通过 `platform-llm` 调模型 ## 9. 本次验收标准 本次实现完成后,应满足: 1. `server-rs` workspace 能识别 `platform-llm` crate 2. `cargo test -p platform-llm` 通过 3. `cargo check -p platform-llm` 通过 4. `platform-llm` README 不再是“仅目录占位” 5. `docs/technical/README.md` 有正式文档索引