# `api-server` 接入 `platform-llm` 最小代理设计(2026-04-21) ## 1. 目标 在 `platform-llm` 已落成真实 Rust crate 后,`api-server` 需要尽快拥有一条可正式消费的平台接线面,避免平台层只停留在“可编译但未接入”状态。 本次目标只做最小闭环: 1. 在 `api-server` 配置层补齐 LLM 文本网关环境变量 2. 在 `AppState` 注入 `platform-llm::LlmClient` 3. 提供 `/api/llm/chat/completions` 非流式兼容代理 4. 保持与旧 Node 路由的鉴权位置和基本请求形态一致 ## 2. 本次范围 ### 2.1 本次实现 1. `AppConfig` 新增 LLM provider / base url / api key / model / timeout / retry 配置 2. `AppState` 初始化 `LlmClient` 3. 新增 `shared-contracts::llm` 4. 新增 `api-server/src/llm.rs` 5. 路由挂载到 `/api/llm/chat/completions` ### 2.2 本次不实现 1. 不实现 SSE 流式透传 2. 不实现通用原样 body 转发 3. 不实现媒体模型路由 4. 不把 `module-ai` 编排接进来 ## 3. 兼容口径 保持与旧 Node `POST /api/llm/chat/completions` 一致的基本语义: 1. 需要登录态 2. 接收 `model? + stream + messages[]` 3. 当前 `stream=true` 明确返回 `501`,避免伪装支持 4. 非流式返回统一后的文本结果,而不是原样上游 JSON ## 4. 返回结构 Rust 首版返回: 1. `id` 2. `model` 3. `content` 4. `finishReason` 原因: 1. 当前 Rust 平台层已经把上游 `choices[0].message.content` 归一完成 2. `api-server` 首版先保持稳定、可消费的文本结果接口 3. 真正需要 OpenAI 完全兼容响应体时,再单独补“原样代理模式” ## 5. 验收 1. `api-server` 能在配置合法时成功构建 `AppState` 2. `/api/llm/chat/completions` 能通过测试打到 mock 上游 3. `stream=true` 返回明确错误 4. crate 级 `check/test` 通过 ## 6. 环境变量与默认值 `api-server` 首版按以下优先级解析 LLM 配置,保证兼容仓库现有 `.env` 口径: 1. provider:`GENARRATIVE_LLM_PROVIDER` -> `LLM_PROVIDER` 2. base url:`GENARRATIVE_LLM_BASE_URL` -> `LLM_BASE_URL` 3. api key:`GENARRATIVE_LLM_API_KEY` -> `LLM_API_KEY` -> `ARK_API_KEY` 4. model:`GENARRATIVE_LLM_MODEL` -> `LLM_MODEL` -> `VITE_LLM_MODEL` 5. timeout:`GENARRATIVE_LLM_REQUEST_TIMEOUT_MS` -> `LLM_REQUEST_TIMEOUT_MS` 6. max retries:`GENARRATIVE_LLM_MAX_RETRIES` -> `LLM_MAX_RETRIES` 7. retry backoff:`GENARRATIVE_LLM_RETRY_BACKOFF_MS` -> `LLM_RETRY_BACKOFF_MS` 默认值统一对齐 `platform-llm`: 1. provider:`ark` 2. base url:`https://ark.cn-beijing.volces.com/api/v3` 3. model:`doubao-1-5-pro-32k-character-250715` 4. request timeout:`30000` 5. max retries:`1` 6. retry backoff:`500` 补充约束: 1. 如果 `api key` 未配置,`api-server` 允许继续启动,但 `/api/llm/chat/completions` 返回 `503` 2. 如果 provider 字符串非法,回退到默认 `ark`,避免因为环境变量拼写问题阻断开发态服务 ## 7. 错误映射 `platform-llm` 到 HTTP 的错误映射固定如下: 1. `InvalidRequest` -> `400 BAD_REQUEST` 2. `InvalidConfig` -> `503 SERVICE_UNAVAILABLE` 3. `Timeout` / `Connectivity` / `Transport` / `Deserialize` / `EmptyResponse` / `StreamUnavailable` -> `502 BAD_GATEWAY` 4. `Upstream(status=429)` -> `429 TOO_MANY_REQUESTS` 5. 其他 `Upstream` -> `502 BAD_GATEWAY` 6. `stream=true` 首版直接返回 `501 NOT_IMPLEMENTED` ## 8. 角色扮演模型联网搜索补充(2026-04-25) ### 8.1 目标 角色扮演运行时调用文本模型生成剧情正文、NPC 对话、战斗演出文本时,需要默认允许模型使用上游联网搜索能力,提升现实题材、时代背景、地名器物、文化细节的准确度。 ### 8.2 落地范围 1. `platform-llm` 的 `LlmTextRequest` 增加 `enable_web_search` 布尔开关,默认 `false`,避免影响普通平台代理和非剧情调用。 2. `api-server` 配置增加 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED` / `RPG_LLM_WEB_SEARCH_ENABLED`,默认 `true`。 3. 仅 `runtime_story` 兼容链路中的角色扮演剧情文本请求按配置开启联网搜索。 4. `/api/llm/chat/completions` 通用代理不默认开启联网搜索,避免外部调用方在无感情况下产生额外成本或不可预期内容来源。 ### 8.3 上游请求口径 1. 当前默认文本模型走火山方舟 OpenAI 兼容 Chat Completions 路由。 2. 联网搜索开启时,请求体追加 `web_search_options: {}`;关闭时不序列化该字段。 3. 若后续迁移到 Responses API 或更换 provider,由 `platform-llm` 统一收口字段映射,业务层仍只使用 `enable_web_search` 语义开关。 ### 8.4 验收 1. `platform-llm` 单测能捕获开启搜索时上游 JSON 包含 `web_search_options`。 2. `api-server` 配置单测能验证角色扮演搜索开关默认开启、环境变量可关闭。 3. 角色扮演剧情、NPC 对话、推理战斗文本请求都通过同一辅助函数设置搜索开关,避免漏接。 ## 9. AgentSession 创作问答联网搜索补充(2026-04-26) ### 9.1 目标 AgentSession 页面中的 RPG 世界共创、拼图共创、大鱼吃小鱼共创都属于创作问答链路。用户在这些页面里会要求模型补充现实题材、历史文化、地理器物、玩法参照与美术风格依据,因此创作 Agent 的文本问答默认开启上游联网搜索能力。 ### 9.2 落地范围 1. `api-server` 配置增加 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED` / `CREATION_AGENT_LLM_WEB_SEARCH_ENABLED`,默认 `true`。 2. `creation_agent_llm_turn` 作为三类 Agent 共用 LLM 骨架,必须接收显式 `enable_web_search` 参数,并在 `LlmTextRequest` 上设置该值。 3. RPG 世界共创、拼图共创、大鱼吃小鱼共创的普通消息接口与 SSE 流式消息接口都传入同一配置值,避免只有某一种入口开启。 4. RPG 世界共创里的动态状态推断属于对当前聊天状态的结构化判断,不需要联网搜索,继续保持默认关闭。 5. `/api/llm/chat/completions` 通用代理继续不默认开启联网搜索。 ### 9.3 验收 1. `api-server` 配置单测覆盖创作 Agent 联网搜索开关默认开启、环境变量可关闭。 2. 创作 Agent 的共用 LLM 单测覆盖开启搜索时 `LlmTextRequest.enable_web_search` 为 `true`。 3. 三类 Agent turn request 均包含 `enable_web_search` 字段,调用点全部来自 `state.config.creation_agent_llm_web_search_enabled`。