fix: strip <think> tags from reporter output to prevent thinking text leakage (#781) (#862)

* fix: strip <think> tags from LLM output to prevent thinking text leakage (#781)

Some models (e.g. DeepSeek-R1, QwQ via ollama) embed reasoning in
content using <think>...</think> tags instead of the separate
reasoning_content field. This causes thinking text to leak into
both streamed messages and the final report.

Fix at two layers:
- server/app.py: strip <think> tags in _create_event_stream_message
  so ALL streamed content is filtered (coordinator, planner, etc.)
- graph/nodes.py: strip <think> tags in reporter_node before storing
  final_report (which is not streamed through the event layer)

The regex uses a fast-path check ("<think>" in content) to avoid
unnecessary regex calls on normal content.

* refactor: add defensive check for think tag stripping and add reporter_node tests (#781)

- Add isinstance and fast-path check in reporter_node before regex, consistent with app.py
- Add TestReporterNodeThinkTagStripping with 5 test cases covering various scenarios

* chore: re-trigger review
This commit is contained in:
大猫子
2026-02-16 09:38:17 +08:00
committed by GitHub
parent 06248fa6f1
commit 423f5c829c
4 changed files with 176 additions and 0 deletions

View File

@@ -4,6 +4,7 @@
import json
import logging
import os
import re
from functools import partial
from typing import Annotated, Any, Literal
@@ -900,6 +901,12 @@ def reporter_node(state: State, config: RunnableConfig):
logger.debug(f"Current invoke messages: {invoke_messages}")
response = get_llm_by_type(AGENT_LLM_MAP["reporter"]).invoke(invoke_messages)
response_content = response.content
# Strip <think>...</think> tags that some models (e.g. QwQ, DeepSeek) embed
# directly in content instead of using the reasoning_content field (#781)
if isinstance(response_content, str) and "<think>" in response_content:
response_content = re.sub(
r"<think>[\s\S]*?</think>", "", response_content
).strip()
logger.info(f"reporter response: {response_content}")
return {

View File

@@ -6,6 +6,7 @@ import base64
import json
import logging
import os
import re
from typing import Annotated, Any, List, Optional, cast
from uuid import uuid4
@@ -423,6 +424,11 @@ def _create_event_stream_message(
if not isinstance(content, str):
content = json.dumps(content, ensure_ascii=False)
# Strip <think>...</think> tags that some models (e.g. DeepSeek-R1, QwQ via ollama)
# embed directly in content instead of using the reasoning_content field (#781)
if isinstance(content, str) and "<think>" in content:
content = re.sub(r"<think>[\s\S]*?</think>", "", content).strip()
event_stream_message = {
"thread_id": thread_id,
"agent": agent_name,