fix(assistant): 上游返回空串 reasoning_content 时也保留该字段

按字段存在性 (is not None) 判断, 区分"上游未返回"与"返回空串";
流式 round_end、chat_service 下一轮请求、非流式工具循环三处均补齐, 并加回归用例。

Signed-off-by: kevin9327 <5299031+kevin9327@users.noreply.github.com>
This commit is contained in:
kevin9327
2026-10-31 22:01:16 +09:00
parent 7a712fe258
commit f9c4a0ac96
4 changed files with 30 additions and 12 deletions
+3 -3
View File
@@ -268,14 +268,14 @@ async def chat_stream(
while True:
round_text: list[str] = []
tool_calls: list[dict[str, Any]] = []
reasoning_content = ""
reasoning_content: str | None = None
async for event in stream_round(req_messages, schemas, temperature=0.3, timeout=240.0):
if event["type"] == "text":
round_text.append(event["delta"])
await queue.put({"type": "delta", "content": event["delta"]})
elif event["type"] == "round_end":
tool_calls = event.get("tool_calls") or []
reasoning_content = event.get("reasoning_content") or ""
reasoning_content = event.get("reasoning_content")
if not tool_calls:
if not round_text and not text_seen:
@@ -322,7 +322,7 @@ async def chat_stream(
assistant_message["content"] = "".join(round_text)
text_seen = True
# DeepSeek 思考模式要求工具轮的 reasoning_content 原样回传, 否则下一轮 400。
if reasoning_content:
if reasoning_content is not None:
assistant_message["reasoning_content"] = reasoning_content
req_messages.append(assistant_message)
+5 -1
View File
@@ -125,6 +125,8 @@ async def stream_openai_round(
content_seen = False
reasoning_seen = False
reasoning_parts: list[str] = []
# 上游显式返回 reasoning_content (哪怕是空串) 时也要在 round_end 里回传该字段。
reasoning_field_seen = False
finish_reason = ""
calls: dict[int, dict[str, Any]] = {}
ordered: list[dict[str, Any]] = []
@@ -141,6 +143,8 @@ async def stream_openai_round(
if delta is None:
continue
reasoning = getattr(delta, "reasoning_content", None)
if reasoning is not None:
reasoning_field_seen = True
if reasoning:
reasoning_seen = True
reasoning_parts.append(str(reasoning))
@@ -191,7 +195,7 @@ async def stream_openai_round(
"finish_reason": finish_reason,
}
# DeepSeek 思考模式: 带 tools 的后续请求缺了这一轮的 reasoning_content 会被 400。
if reasoning_parts:
if reasoning_field_seen:
end["reasoning_content"] = "".join(reasoning_parts)
yield end
return
+1 -1
View File
@@ -426,7 +426,7 @@ async def generate_ai_text_with_tools(
assistant["content"] = message.content
# DeepSeek 思考模式要求工具轮的 reasoning_content 原样回传, 否则下一轮 400。
reasoning = getattr(message, "reasoning_content", None)
if reasoning:
if reasoning is not None:
assistant["reasoning_content"] = reasoning
req_messages.append(assistant)
@@ -83,11 +83,24 @@ async def test_stream_round_end_without_reasoning_has_no_key(monkeypatch: pytest
assert "reasoning_content" not in events[-1]
async def test_chat_service_sends_reasoning_content_back(monkeypatch: pytest.MonkeyPatch) -> None:
async def test_stream_round_end_keeps_empty_reasoning_content(monkeypatch: pytest.MonkeyPatch) -> None:
# 上游显式返回空串时也要保留字段, 与"未返回该字段"区分开。
events = await _events(monkeypatch, [
_chunk({"reasoning_content": ""}),
_chunk({"tool_calls": [{"index": 0, "id": "call_1", "type": "function",
"function": {"name": "list_strategies", "arguments": "{}"}}]},
finish_reason="tool_calls"),
])
assert events[-1]["reasoning_content"] == ""
@pytest.mark.parametrize("reasoning", ["先看看有哪些策略", ""])
async def test_chat_service_sends_reasoning_content_back(monkeypatch: pytest.MonkeyPatch, reasoning: str) -> None:
captured: list[list[dict[str, Any]]] = []
script = [
{"tool_calls": [{"id": "call_1", "name": "list_strategies", "arguments": "{}"}],
"reasoning_content": "先看看有哪些策略"},
"reasoning_content": reasoning},
{"text": "你有 3 个策略"},
]
@@ -97,7 +110,7 @@ async def test_chat_service_sends_reasoning_content_back(monkeypatch: pytest.Mon
if step.get("text"):
yield {"type": "text", "delta": step["text"]}
end = {"type": "round_end", "tool_calls": step.get("tool_calls", []), "finish_reason": "stop"}
if step.get("reasoning_content"):
if "reasoning_content" in step:
end["reasoning_content"] = step["reasoning_content"]
yield end
@@ -115,15 +128,16 @@ async def test_chat_service_sends_reasoning_content_back(monkeypatch: pytest.Mon
assert [json.loads(line) for line in lines if '"error"' in line] == []
assistant_msg = captured[1][-2]
assert assistant_msg["role"] == "assistant"
assert assistant_msg["reasoning_content"] == "先看看有哪些策略"
assert assistant_msg["reasoning_content"] == reasoning
async def test_tool_loop_sends_reasoning_content_back(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.mark.parametrize("reasoning", ["先跑一次回测", ""])
async def test_tool_loop_sends_reasoning_content_back(monkeypatch: pytest.MonkeyPatch, reasoning: str) -> None:
replies = [
ChatCompletionMessage.model_validate({
"role": "assistant",
"content": None,
"reasoning_content": "先跑一次回测",
"reasoning_content": reasoning,
"tool_calls": [{"id": "call_1", "type": "function",
"function": {"name": "run_backtest", "arguments": "{}"}}],
}),
@@ -147,5 +161,5 @@ async def test_tool_loop_sends_reasoning_content_back(monkeypatch: pytest.Monkey
execute_tool=fake_execute,
)
assert messages[1]["reasoning_content"] == "先跑一次回测"
assert messages[1]["reasoning_content"] == reasoning
assert "reasoning_content" not in messages[3]