provider 模式下 tool-plan 请求恒为非流式,llm.stream 配置对其不生效 #186

Closed
opened 2026-08-24 19:13:36 +08:00 by lhk229 · 0 comments
Owner

provider 模式下 tool-plan 请求恒为非流式,llm.stream 配置对其不生效

摘要

agentMode: provider 时,tool-plan / 强制动作类请求永远走非流式(client.run()),配置项 llm.stream 在这条路径上只被写进审计记录和重试指纹,不驱动任何行为。

对只接受流式请求的 OpenAI 兼容网关,这导致第一发请求就确定性失败,且因为返回 400 不在可重试状态集合内,不会重试,2 秒内整轮结束。

这不是上游抖动——同一配置下 100% 复现。

现象

配置:

{
  "agentMode": "provider",
  "llm": {
    "apiKind": "openai_responses",
    "baseUrl": "http://<gateway>/v1",
    "model": "gpt-5.6-luna",
    "stream": true,
    "reasoningEffort": "medium",
    "maxRetries": 2,
    "retryBackoffMs": 500,
    "requestTimeoutMs": 600000
  }
}

启动立项策划后约 2 秒:

任务已接收,项目总控 Agent 正在启动处理。
项目总控 Agent 服务请求失败,请稍后重试
项目总控 Agent 执行失败,请稍后重试
本轮策划已停止。

runtime 事件流(.agent/runtime/events/project-supervisor.jsonl):

[1787566617] turn.started    running   Agent Runtime 开始处理本轮输入。
[1787566617] turn.progress   running   生成 Agent 工具计划(第 1 轮)
[1787566619] error           failed    项目总控 Agent 服务请求失败,请稍后重试
[1787566619] turn.failed     failed    项目总控 Agent 服务请求失败,请稍后重试

agent.db 中无任何 provider_request.retry / retry_waiting 记录——一次都没重试。lifecycle 只有一对 started → failed(slot loop-1-repair-0),没有 -transient-N

根因

1. 上游拒绝的是非流式请求

平台层自带 raw failure log(logs/llm-raw/,相对进程 cwd,可用环境变量 LLM_RAW_LOG_DIR 覆盖)原样保留了上游响应:

logs/llm-raw/<ts>-<pid>-<seq>-upstream_status_failed.output.txt

{"error":{"message":"Stream must be set to true","type":"bad_response_status_code","param":"","code":"bad_response_status_code"}}

同名 .input.json 记录了我们实际发出的请求:

provider        = openai_compatible
apiKind         = openai_responses
model           = gpt-5.6-luna
stream          = false          ← 配置里是 true
attempt         = 1
maxOutputTokens = 2000

该请求体是 plan 根 Supervisor 的第一发 tool-plan,被阶段策略强制为只能调用 agent.goal_contractmaxOutputTokens = 2000 对应 AGENT_RUNTIME_AUTONOMOUS_FORCED_ACTION_MAX_OUTPUT_TOKENS)。

2. LlmRunRequest 没有 stream 字段

server-rs/crates/platform-llm/src/lib.rs:167 —— 流式与否不是请求的属性,而是由调用哪个方法决定:

  • LlmClient::run()lib.rs:1528)—— 非流式
  • LlmClient::stream_run()lib.rs:1574)—— 流式

3. 全仓库只有三处走流式

调用点 用途
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_final_reply.rs:274 最终回复
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs:522 聊天入口
apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs:499 生成循环

tool-plan 走的是 apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs:1700

GAME_CREATOR_AGENT_MODE_PROVIDER => {
    let client = build_game_creator_agent_runtime_llm_client(
        &llm_for_request,
        &config_path_for_request,
    )
    .map_err(platform_llm::LlmError::InvalidConfig)?;
    client.run(request).await          // 恒非流式,不读 llm.stream
}

4. llm.stream 在这条路径上只被"记录"

同文件内仅两处引用,均不影响行为:

  • provider_retry.rs:515 —— 写进 PlanProviderRequestContextValue.stream 审计字段
  • provider_retry.rs:627 —— 参与 Provider 重试配置指纹的 sha256

也就是说,审计记录会显示 stream: true,而实际发出的请求是 stream: false——审计与事实不一致,这本身也值得单独关注。

5. 400 不可重试,所以秒挂

重试状态白名单只含 429 / 500 / 502 / 503。本次是 400,直接终态失败。

对比:同一网关早前一次失败返回的是 500(do_request_failed),属于可重试,运行时转了一圈 -transient-1 后才失败,耗时 4 秒。两者的差别只是"挂得多快",最终都失败。

三种模式组合的表现

组合 结果 原因
网关 + codex_app_server 可运行 Codex app-server 自行发起请求,走它自己的流式实现
网关 + provider 必定失败 tool-plan 恒非流式,网关返回 400
官方端点 + 任意模式 可运行 官方端点流式/非流式都接受

复现步骤

  1. agentMode 设为 providerapiKind 设为 openai_responses
  2. baseUrl 指向一个要求 stream=true 的 OpenAI 兼容网关
  3. llm.stream 设为 true
  4. 启动立项策划
  5. 约 2 秒后失败;查看 logs/llm-raw/ 下最新的 *-upstream_status_failed.* 一对文件

期望行为

以下二选一:

A. 让 llm.stream 真正生效 —— tool-plan 等 provider 请求按配置走 stream_run()

代价较大:流式响应下的工具调用装配、reject_incomplete_tool_calls 的判定时机、tool-plan 协议解析都要走另一套路径。该路径为 provider 模式下所有 Agent 共用,改动面覆盖全部创作链路。

B. 让配置面诚实 —— 在配置校验层明确 llm.stream 仅作用于最终回复/聊天/生成循环,对 tool-plan 无效;或在 agentMode=providerstream=true 时给出提示。

代价小,无行为变更,但不解决网关可用性。

附注

审计字段与事实不一致

PlanProviderRequestContextValue.stream 与 Provider 重试指纹都记录 llm.stream配置值而非实际值。排查时若只看审计记录会被误导(本次即是如此——审计显示 stream: true,实际发出 stream: false,只有 raw log 暴露了真相)。

raw failure log 目录会持续增长

排查时发现 apps/ai-game-creator-shell/src-tauri/logs/llm-raw/ 已累积 3278 个文件,无轮转或清理机制。每次失败写入一对 .input.json + .output.txt,其中 .input.json 通常数十 KB。建议评估是否需要保留上限。

该网关本身另有稳定性问题(不同问题,建议分开跟踪)

同一网关在 codex_app_server 模式下可运行,但实测:

  • 29 次 provider 请求中 9 次失败(约 31%)
  • 其中两次请求在飞 192 秒 / 196 秒 后连接被丢弃且未返回任何终态事件(正常请求为 8–31 秒),触发 runtime 的 fail-closed 逻辑进入 needs-reconciliation
  • 换用官方端点后,同样链路 14 次请求 0 失败

195 秒这个时长与本仓库任何超时配置都不匹配(requestTimeoutMs 600s、Codex RPC 30s、Codex direct 900s/6600s/7200s),推测为网关侧连接保持上限。

# provider 模式下 tool-plan 请求恒为非流式,`llm.stream` 配置对其不生效 ## 摘要 `agentMode: provider` 时,tool-plan / 强制动作类请求**永远**走非流式(`client.run()`),配置项 `llm.stream` 在这条路径上只被写进审计记录和重试指纹,不驱动任何行为。 对只接受流式请求的 OpenAI 兼容网关,这导致**第一发请求就确定性失败**,且因为返回 400 不在可重试状态集合内,不会重试,2 秒内整轮结束。 这不是上游抖动——同一配置下 100% 复现。 ## 现象 配置: ```json { "agentMode": "provider", "llm": { "apiKind": "openai_responses", "baseUrl": "http://<gateway>/v1", "model": "gpt-5.6-luna", "stream": true, "reasoningEffort": "medium", "maxRetries": 2, "retryBackoffMs": 500, "requestTimeoutMs": 600000 } } ``` 启动立项策划后约 2 秒: ``` 任务已接收,项目总控 Agent 正在启动处理。 项目总控 Agent 服务请求失败,请稍后重试 项目总控 Agent 执行失败,请稍后重试 本轮策划已停止。 ``` runtime 事件流(`.agent/runtime/events/project-supervisor.jsonl`): ``` [1787566617] turn.started running Agent Runtime 开始处理本轮输入。 [1787566617] turn.progress running 生成 Agent 工具计划(第 1 轮) [1787566619] error failed 项目总控 Agent 服务请求失败,请稍后重试 [1787566619] turn.failed failed 项目总控 Agent 服务请求失败,请稍后重试 ``` `agent.db` 中无任何 `provider_request.retry` / `retry_waiting` 记录——**一次都没重试**。lifecycle 只有一对 `started → failed`(slot `loop-1-repair-0`),没有 `-transient-N`。 ## 根因 ### 1. 上游拒绝的是非流式请求 平台层自带 raw failure log(`logs/llm-raw/`,相对进程 cwd,可用环境变量 `LLM_RAW_LOG_DIR` 覆盖)原样保留了上游响应: `logs/llm-raw/<ts>-<pid>-<seq>-upstream_status_failed.output.txt` ```json {"error":{"message":"Stream must be set to true","type":"bad_response_status_code","param":"","code":"bad_response_status_code"}} ``` 同名 `.input.json` 记录了我们实际发出的请求: ``` provider = openai_compatible apiKind = openai_responses model = gpt-5.6-luna stream = false ← 配置里是 true attempt = 1 maxOutputTokens = 2000 ``` 该请求体是 plan 根 Supervisor 的第一发 tool-plan,被阶段策略强制为只能调用 `agent.goal_contract`(`maxOutputTokens = 2000` 对应 `AGENT_RUNTIME_AUTONOMOUS_FORCED_ACTION_MAX_OUTPUT_TOKENS`)。 ### 2. `LlmRunRequest` 没有 `stream` 字段 `server-rs/crates/platform-llm/src/lib.rs:167` —— 流式与否**不是请求的属性**,而是由调用哪个方法决定: - `LlmClient::run()`(`lib.rs:1528`)—— 非流式 - `LlmClient::stream_run()`(`lib.rs:1574`)—— 流式 ### 3. 全仓库只有三处走流式 | 调用点 | 用途 | |---|---| | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_final_reply.rs:274` | 最终回复 | | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs:522` | 聊天入口 | | `apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs:499` | 生成循环 | tool-plan 走的是 `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs:1700`: ```rust GAME_CREATOR_AGENT_MODE_PROVIDER => { let client = build_game_creator_agent_runtime_llm_client( &llm_for_request, &config_path_for_request, ) .map_err(platform_llm::LlmError::InvalidConfig)?; client.run(request).await // 恒非流式,不读 llm.stream } ``` ### 4. `llm.stream` 在这条路径上只被"记录" 同文件内仅两处引用,均不影响行为: - `provider_retry.rs:515` —— 写进 `PlanProviderRequestContextValue.stream` 审计字段 - `provider_retry.rs:627` —— 参与 Provider 重试配置指纹的 sha256 也就是说,审计记录会显示 `stream: true`,而实际发出的请求是 `stream: false`——**审计与事实不一致**,这本身也值得单独关注。 ### 5. 400 不可重试,所以秒挂 重试状态白名单只含 429 / 500 / 502 / 503。本次是 400,直接终态失败。 对比:同一网关早前一次失败返回的是 500(`do_request_failed`),属于可重试,运行时转了一圈 `-transient-1` 后才失败,耗时 4 秒。两者的差别只是"挂得多快",最终都失败。 ## 三种模式组合的表现 | 组合 | 结果 | 原因 | |---|---|---| | 网关 + `codex_app_server` | 可运行 | Codex app-server 自行发起请求,走它自己的流式实现 | | 网关 + `provider` | **必定失败** | tool-plan 恒非流式,网关返回 400 | | 官方端点 + 任意模式 | 可运行 | 官方端点流式/非流式都接受 | ## 复现步骤 1. `agentMode` 设为 `provider`,`apiKind` 设为 `openai_responses` 2. `baseUrl` 指向一个要求 `stream=true` 的 OpenAI 兼容网关 3. `llm.stream` 设为 `true` 4. 启动立项策划 5. 约 2 秒后失败;查看 `logs/llm-raw/` 下最新的 `*-upstream_status_failed.*` 一对文件 ## 期望行为 以下二选一: **A. 让 `llm.stream` 真正生效** —— tool-plan 等 provider 请求按配置走 `stream_run()`。 代价较大:流式响应下的工具调用装配、`reject_incomplete_tool_calls` 的判定时机、tool-plan 协议解析都要走另一套路径。该路径为 provider 模式下**所有** Agent 共用,改动面覆盖全部创作链路。 **B. 让配置面诚实** —— 在配置校验层明确 `llm.stream` 仅作用于最终回复/聊天/生成循环,对 tool-plan 无效;或在 `agentMode=provider` 且 `stream=true` 时给出提示。 代价小,无行为变更,但不解决网关可用性。 ## 附注 ### 审计字段与事实不一致 `PlanProviderRequestContextValue.stream` 与 Provider 重试指纹都记录 `llm.stream` 的**配置值**而非**实际值**。排查时若只看审计记录会被误导(本次即是如此——审计显示 `stream: true`,实际发出 `stream: false`,只有 raw log 暴露了真相)。 ### raw failure log 目录会持续增长 排查时发现 `apps/ai-game-creator-shell/src-tauri/logs/llm-raw/` 已累积 3278 个文件,无轮转或清理机制。每次失败写入一对 `.input.json` + `.output.txt`,其中 `.input.json` 通常数十 KB。建议评估是否需要保留上限。 ### 该网关本身另有稳定性问题(不同问题,建议分开跟踪) 同一网关在 `codex_app_server` 模式下可运行,但实测: - 29 次 provider 请求中 9 次失败(约 31%) - 其中两次请求在飞 **192 秒 / 196 秒** 后连接被丢弃且未返回任何终态事件(正常请求为 8–31 秒),触发 runtime 的 fail-closed 逻辑进入 `needs-reconciliation` - 换用官方端点后,同样链路 14 次请求 0 失败 195 秒这个时长与本仓库任何超时配置都不匹配(`requestTimeoutMs` 600s、Codex RPC 30s、Codex direct 900s/6600s/7200s),推测为网关侧连接保持上限。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#186