fix: sanitize fabricated tool results everywhere

This commit is contained in:
666ghj 2026-07-22 01:24:00 +08:00
parent 1d96b09875
commit f0e379471a
2 changed files with 59 additions and 13 deletions

View File

@ -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'<tool_result>.*?</tool_result>',
'',
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'<tool_result\b.*$', '', cleaned, flags=re.IGNORECASE | re.DOTALL)
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
return cleaned.strip()
@ -1399,9 +1413,9 @@ class ReportAgent:
# ── 情况1LLM 输出了 Final Answer ──
if has_final_answer:
cleaned_response = ReportAgent._strip_fake_tool_results(response)
# 工具调用次数不足,拒绝并要求继续调工具
if tool_calls_count < min_tool_calls:
cleaned_response = ReportAgent._strip_fake_tool_results(response)
messages.append({"role": "assistant", "content": cleaned_response})
unused_tools = all_tools - used_tools
unused_hint = f"(这些工具还未使用,推荐用一下他们: {', '.join(unused_tools)}" if unused_tools else ""
@ -1416,7 +1430,7 @@ class ReportAgent:
continue
# 正常结束
final_answer = response.split("Final Answer:")[-1].strip()
final_answer = cleaned_response.split("Final Answer:")[-1].strip()
logger.info(t('report.sectionGenDone', title=section.title, count=tool_calls_count))
if self.report_logger:
@ -1432,7 +1446,8 @@ class ReportAgent:
if has_tool_calls:
# 工具额度已耗尽 → 明确告知,要求输出 Final Answer
if tool_calls_count >= 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'<tool_call>.*?</tool_call>', '', 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'<tool_call>.*?</tool_call>', '', 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(),

View File

@ -0,0 +1,28 @@
import pytest
from app.services.report_agent import ReportAgent
@pytest.mark.parametrize(
("response", "expected"),
[
("before <tool_result>fake</tool_result> after", "before after"),
("before\n<tool_result>line 1\nline 2</tool_result>\nafter", "before\n\nafter"),
("a<TOOL_RESULT source='model'>fake</TOOL_RESULT>b", "ab"),
(
"a<tool_result>outer<tool_result>inner</tool_result>end</tool_result>b",
"ab",
),
("safe<tool_result>unclosed fake", "safe"),
("safe<tool_result malformed", "safe"),
("a</tool_result>b", "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: <tool_call>{}</tool_call> legitimate text"
assert ReportAgent._strip_fake_tool_results(response) == response