902f487ea9
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>
platform-llm 平台适配 crate
日期:2026-07-27
1. crate 职责
platform-llm 是 Rust 工作区里的大模型平台适配 crate,当前已经落地以下能力:
- 统一 Ark / DashScope / Anthropic / 其他兼容网关的文本模型配置结构
- 统一 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 的文本 / 原生 function tool 请求、非流式响应与 SSE 流式解析
- 统一超时、连接失败、上游错误、空响应与重试策略
- 为后续
module-ai、module-story、module-npc、module-custom-world提供可直接复用的基础 client
2. 当前能力边界
当前实现只覆盖“文本 run”主链,不提前混入媒体生成和业务编排:
- 对外抽象固定为
LlmRunRequest/LlmRunResponse,不再保留旧LlmTextRequest/LlmTextResponse类型。 OpenAiChat、OpenAiResponses与Anthropic三类 API kind 都支持 JSON 请求、非流式响应和 SSE 流式响应;默认 API kind 仍为OpenAiResponses。- 三类协议都使用统一的
function_tools/tool_choice输入和LlmRunResponse.tool_calls输出。Anthropic 请求使用顶层tools[].input_schema与对象形态tool_choice;Anthropic URL 默认在 base URL 后拼/v1/messages,如果 base URL 已以/v1结尾则只拼/messages。 - Anthropic 当前仍不支持
web_search、图片内容和纯 system 消息;至少需要一条非 system 文本消息。角色动画、图片、视频、资产轮询仍留在其他平台适配和业务模块任务里。 - 流式
on_delta只发送文本增量与完成原因;工具调用增量在 crate 内按 slot 聚合,完整调用只从最终LlmRunResponse.tool_calls读取。上下文管理、后台执行和业务状态不写回本 crate。 - 支持按 provider 打标签,但不把业务 prompt、SSE 转发和模块状态写回本 crate。
DashScope当前只通过“调用方显式提供兼容文本网关 base url”的方式接入,不复用图像 API。- 角色动画、图片、视频、资产轮询仍留在后续
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. 流式与参数契约
LlmStreamDelta只包含accumulated_text、delta_text和finish_reason,工具调用不会进入on_delta;纯工具响应允许text为空。- 工具片段按协议索引聚合:Chat 使用
delta.tool_calls[].index,Responses 使用output_index,Anthropic 使用 content blockindex。Responses 的.done、response.completed和response.incomplete中的完整 arguments 是权威值,可以覆盖之前的分片拼接。 - 流结束固化工具调用时,缺少 id 或函数名返回
Deserialize;空参数默认保存为{};非空参数必须能反序列化为完整 JSON,截断或半截 JSON 不会交给业务层。这里是 JSON 语法完整性检查,不是针对parameters的 JSON Schema 业务校验。 - 非流式工具调用采用不同的参数边界:缺失或空白
arguments统一归一为{};Chat / Responses 的非空畸形arguments不在平台层做 JSON 校验、修复或静默丢弃,而是保留参数内容(仅按统一归一策略去除首尾空白),连同 call id 和函数名交给调用方的 repair 循环。Anthropictool_use.input缺失时同样按{}归一;调用方不能把非流式参数自动假定为统一 schema 校验通过。
5. 错误边界
Deserialize:上游 JSON、SSE 或 UTF-8 无法解析,Chat 非流式缺少choices[0],流式工具调用缺少 id / name,或流式工具参数不是完整 JSON。StreamUnavailable:仅用于流式响应已声明tool_use/tool_calls,但一个工具 slot 都没有聚合出来的协议兼容失败;调用方可以据此回退一次非流式请求,不能把已收到的解说文本当成最终回复。EmptyResponse:最终文本为空且工具调用也为空。只有文本为空但存在有效工具调用时,响应才是合法的纯工具响应。- 流尾部出现
Timeout、Connectivity、Transport或Deserialize时,只有已形成非空文本或至少一个工具调用、观察到协议完成信号、已有完成原因且工具参数完整的响应才会保留;纯工具响应即使text为空也可以保留。工具参数半截或既没有文本也没有工具调用时继续返回错误。
6. 核心导出
当前对外导出以下公共类型:
LlmProviderLlmConfigLlmMessageRoleLlmMessageLlmRunRequestLlmApiKindLlmStreamDeltaLlmFunctionToolLlmToolChoiceLlmToolCallLlmRunResponseLlmTokenUsageLlmClientLlmError
7. 设计文档
当前文档入口
当前长期工程口径已融合到:
- ../../../docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md
- ../../../docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
- ../../../docs/【开发运维】本地开发验证与生产运维-2026-05-15.md
旧阶段设计文档不再作为实现依据。
8. 边界约束
platform-llm只承接模型平台适配,不承接业务模块状态真相与业务规则。- 业务模块只能依赖这里的统一 client / DTO / 错误模型,不能再把上游请求细节散落回各 crate。
api-server后续如果需要做 REST/SSE façade,只允许在协议层调用platform-llm,不能复制一份私有实现。
9. 验收证据边界
cargo test -p platform-llm的确定性用例把 checked-in SSE fixture 交给 parser,验证归一后的文本、工具调用、slot 聚合、Responses 仅有 completed / incomplete 终态事件时的恢复、参数 JSON 完整性和错误边界。fixture 可以来源于真实端点抓包,但测试不保存原始 SSE,也不逐事件与端点报文比较,因此不能证明抓包转录无偏差。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 录制或逐事件对比能力。- 因此验收应分别称为“固定 SSE fixture parser 覆盖”和“真实端点归一工具调用 smoke”,不能把后者描述为原始 SSE fidelity 或转录一致性证明。