Files
Genarrative/server-rs/crates/platform-llm
lhk229 902f487ea9
Project CI / Frontend tests (pull_request) Failing after 23s
Project CI / Repository checks (pull_request) Failing after 46s
Project CI / Backend tests (pull_request) Successful in 2m55s
Project CI / Native shell tests (pull_request) Successful in 10m6s
Responses 终态事件恢复正文
response.completed / incomplete 分支只从终态载荷恢复工具调用,不恢复
message.content[].output_text,导致三种后果(均已实测复现):

- 纯文本的 completed-only 响应 -> EmptyResponse,上层白跑一轮重试或降级
- response.incomplete 携带的截断正文同样退化成 EmptyResponse,连降级
  回复都给不出来
- 「正文 + 工具调用」响应不报错,但模型的前置说明被静默丢掉,最隐蔽

有增量事件的正常流和非流式路径都不受影响。

归属:终态载荷成为「恢复源」是本分支 7c61e9a53 确立的,它加了
extract_responses_completed_tool_fragments 却没同步提取正文,且同一提交新增
的回归载荷里带着「我来查询。」而只断言 tool_calls,缺陷被自己的测试盖住;
fc53ee5c9 把 incomplete 并进同一分支时把这个不对称一起带了过去。更早的
9d684cb7b 虽然也没提正文,但那时终态载荷根本不是恢复源。

修法:ParsedStreamEvent 新增 text_snapshot,与 delta_text 分开——它不是增量,
按增量累加会让正文翻倍。终态分支把 response 反序列化成
ResponsesResponseEnvelope 后复用 extract_responses_text,不另写裸 JSON 提取器:
后者必然漏掉 reasoning / thinking 等隐藏 part 的过滤,会把思维链当正文吐出去。
反序列化失败按「没有快照」静默降级而非报错——兜底路径放过只是回到修复前行为,
与槽位缺失那类会造成静默错配的失败关闭口径不同。

累加时按状态分两条路:累加为空(只发终态事件的网关)把快照当成一次增量发出去,
否则 response.text 对了但调用方流式通道全程收不到文本(Responses 的
emit_finish_only_delta 为 false,只有 Chat 为真);累加非空则按权威值覆盖且不
补发回调,避免调用方侧翻倍。覆盖语义与 arguments_complete 一致。

隔离验证两次:把 text_snapshot 置 None,四条用例转红;把覆盖改成追加,
不重复用例转红。

platform-llm 95 passed(原 91)。新增 completed-only 纯文本、incomplete-only
纯文本、delta 与终态并存不重复、终态 reasoning part 不入正文四条,并收紧既有的
stream_run_recovers_responses_tool_calls_from_completed_event_only 断言正文。
前两条同时断言 on_delta 实际回调内容,只断言 response.text 抓不到流式通道那个坑。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:42:00 +00:00
..
2026-07-27 11:42:00 +00:00
2026-07-27 10:38:01 +00:00
2026-07-27 10:38:01 +00:00

platform-llm 平台适配 crate

日期:2026-07-27

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 只发送文本增量与完成原因;工具调用增量在 crate 内按 slot 聚合,完整调用只从最终 LlmRunResponse.tool_calls 读取。上下文管理、后台执行和业务状态不写回本 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 包装层和 strict { "type": "auto" } / { "type": "any" };Required 映射为 any content[].type=tool_use,input 序列化为 arguments content block index;content_block_start 提供身份,input_json_delta 拼接参数

Responses 如果只发送 response.completed 或 response.incomplete,解析器会从其中的 response.output[] 恢复 function_call;恢复时使用 output 数组下标作为 slot。response.incomplete 表示上游没有完成本轮生成:其中的工具调用即使参数是完整 JSON 也返回 Deserialize,纯正文则保留为可用的降级结果。三种协议的并行工具调用只在平台层做 slot 聚合,不代表工具会在平台层并发执行。

4. 流式与参数契约

  1. LlmStreamDelta 只包含 accumulated_text、delta_text 和 finish_reason,工具调用不会进入 on_delta;纯工具响应允许 text 为空。
  2. 工具片段按协议索引聚合:Chat 使用 delta.tool_calls[].index,Responses 使用 output_index,Anthropic 使用 content block index。Responses 的 .done、response.completed 和 response.incomplete 中的完整 arguments 是权威值,可以覆盖之前的分片拼接。
  3. 流结束固化工具调用时,缺少 id 或函数名返回 Deserialize;空参数默认保存为 {};非空参数必须能反序列化为完整 JSON,截断或半截 JSON 不会交给业务层。这里是 JSON 语法完整性检查,不是针对 parameters 的 JSON Schema 业务校验。
  4. 非流式工具调用采用不同的参数边界:缺失或空白 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. LlmMessageRole
  4. LlmMessage
  5. LlmRunRequest
  6. LlmApiKind
  7. LlmStreamDelta
  8. LlmFunctionTool
  9. LlmToolChoice
  10. LlmToolCall
  11. LlmRunResponse
  12. LlmTokenUsage
  13. LlmClient
  14. LlmError

7. 设计文档

当前文档入口

当前长期工程口径已融合到:

  1. ../../../docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md
  2. ../../../docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
  3. ../../../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 完整性和错误边界。fixture 可以来源于真实端点抓包,但测试不保存原始 SSE,也不逐事件与端点报文比较,因此不能证明抓包转录无偏差。
  2. tests/live_stream_tool_calls.rs 是默认 #[ignore] 的真实端点工具调用 smoke。PLATFORM_LLM_LIVE_API_KIND 支持 openai_responses、openai_chat 和 anthropic,未设置或空白时默认 openai_responses,未知非空值会直接使验收失败。它只验证最终归一结果中的工具名、id 和完整参数 JSON;文本增量字符数仅用于打印观测,工具调用不进入 on_delta,也没有原始 SSE 录制或逐事件对比能力。
  3. 因此验收应分别称为“固定 SSE fixture parser 覆盖”和“真实端点归一工具调用 smoke”,不能把后者描述为原始 SSE fidelity 或转录一致性证明。