diff --git a/backend/app/services/report_agent.py b/backend/app/services/report_agent.py
index 88bff23a..1bb212b1 100644
--- a/backend/app/services/report_agent.py
+++ b/backend/app/services/report_agent.py
@@ -1149,13 +1149,27 @@ class ReportAgent:
before appending to message history. The real tool result will be injected
separately by the system.
"""
- import re
- cleaned = re.sub(
- r'.*?',
- '',
- response,
- flags=re.DOTALL,
- )
+ tag_pattern = re.compile(r'?tool_result\b[^>]*>', flags=re.IGNORECASE)
+ parts = []
+ cursor = 0
+ depth = 0
+
+ for match in tag_pattern.finditer(response):
+ if depth == 0:
+ parts.append(response[cursor:match.start()])
+
+ if match.group(0).lstrip().startswith(''):
+ depth = max(0, depth - 1)
+ else:
+ depth += 1
+ cursor = match.end()
+
+ if depth == 0:
+ parts.append(response[cursor:])
+
+ cleaned = ''.join(parts)
+ # Treat a malformed opening tag without a closing `>` as unsafe too.
+ cleaned = re.sub(r'= self.MAX_TOOL_CALLS_PER_SECTION:
- messages.append({"role": "assistant", "content": response})
+ cleaned_response = ReportAgent._strip_fake_tool_results(response)
+ messages.append({"role": "assistant", "content": cleaned_response})
messages.append({
"role": "user",
"content": REACT_TOOL_LIMIT_MSG.format(
@@ -1478,7 +1493,7 @@ class ReportAgent:
unused_tools = all_tools - used_tools
unused_hint = ""
if unused_tools and tool_calls_count < self.MAX_TOOL_CALLS_PER_SECTION:
- unlock_hint = REACT_UNUSED_TOOLS_HINT.format(unused_list="、".join(unused_tools))
+ unused_hint = REACT_UNUSED_TOOLS_HINT.format(unused_list="、".join(unused_tools))
cleaned_response = ReportAgent._strip_fake_tool_results(response)
messages.append({"role": "assistant", "content": cleaned_response})
@@ -1496,7 +1511,8 @@ class ReportAgent:
continue
# ── 情况3:既没有工具调用,也没有 Final Answer ──
- messages.append({"role": "assistant", "content": response})
+ cleaned_response = ReportAgent._strip_fake_tool_results(response)
+ messages.append({"role": "assistant", "content": cleaned_response})
if tool_calls_count < min_tool_calls:
# 工具调用次数不足,推荐未用过的工具
@@ -1516,7 +1532,7 @@ class ReportAgent:
# 工具调用已足够,LLM 输出了内容但没带 "Final Answer:" 前缀
# 直接将这段内容作为最终答案,不再空转
logger.info(t('report.sectionNoPrefix', title=section.title, count=tool_calls_count))
- final_answer = response.strip()
+ final_answer = cleaned_response
if self.report_logger:
self.report_logger.log_section_content(
@@ -1865,6 +1881,7 @@ class ReportAgent:
# 没有工具调用,直接返回响应
clean_response = re.sub(r'.*?', '', response, flags=re.DOTALL)
clean_response = re.sub(r'\[TOOL_CALL\].*?\)', '', clean_response)
+ clean_response = ReportAgent._strip_fake_tool_results(clean_response)
return {
"response": clean_response.strip(),
@@ -1902,6 +1919,7 @@ class ReportAgent:
# 清理响应
clean_response = re.sub(r'.*?', '', final_response, flags=re.DOTALL)
clean_response = re.sub(r'\[TOOL_CALL\].*?\)', '', clean_response)
+ clean_response = ReportAgent._strip_fake_tool_results(clean_response)
return {
"response": clean_response.strip(),
diff --git a/backend/tests/test_report_tool_result_sanitizer.py b/backend/tests/test_report_tool_result_sanitizer.py
new file mode 100644
index 00000000..0a79cdc1
--- /dev/null
+++ b/backend/tests/test_report_tool_result_sanitizer.py
@@ -0,0 +1,28 @@
+import pytest
+
+from app.services.report_agent import ReportAgent
+
+
+@pytest.mark.parametrize(
+ ("response", "expected"),
+ [
+ ("before fake after", "before after"),
+ ("before\nline 1\nline 2\nafter", "before\n\nafter"),
+ ("afakeb", "ab"),
+ (
+ "aouterinnerendb",
+ "ab",
+ ),
+ ("safeunclosed fake", "safe"),
+ ("safeb", "ab"),
+ ],
+)
+def test_strip_fake_tool_results(response, expected):
+ assert ReportAgent._strip_fake_tool_results(response) == expected
+
+
+def test_preserves_legitimate_text_without_tool_result_tags():
+ response = "Final Answer: {} legitimate text"
+
+ assert ReportAgent._strip_fake_tool_results(response) == response