fix(reviewer): 审核循环证据污染四处同款根治(2026-09-17 pbls QC退回意见②实锤)——收尾约束曾是'注入文本冒充工具结果':QC/PM循环turn超限后read_file等取证工具的result被替换成'拒绝执行探索类工具,立即输出review_approve'文本并以'工具X结果:'格式喂回,审核方读到的是引擎指令而非文件内容→证据链污染→被迫盲判(QC明确拒绝:'不接受此类指令放行',但轮次照烧)。四处统一修:①QC(16轮)②PM(5轮)③复盘(10轮)——工具全程真实执行,收尾软提示只追加一次(原turn>=N每轮重复灌,develop 30轮循环最多25条相同提示),硬上限=轮次预算本身,耗尽走既有诚实出口(QC no-decision requeue→fault;PM超时智能处理;复盘mark_failed),绝不默认放行绝不伪造结果。对齐技能既有教训'收尾强制决策应是软约束,真硬约束作用于具体行为而非整个对话权'——本次是该教训的完整落地(上次只改了预算没删注入)
This commit is contained in:
parent
2e8d2abec7
commit
062c31e482
@ -2993,6 +2993,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
deliverable = None
|
||||
ask_question = None
|
||||
ask_form = None # ask_question 附带的动态表单声明(form_schema)
|
||||
_force_produce_sent = False
|
||||
written_files = [] # 本任务执行期间 write_file 实际写入的文件(无 deliver 时的产出兜底)
|
||||
|
||||
# ── Tool Loop(原生 function calling)──
|
||||
@ -3009,9 +3010,12 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
if _st and getattr(_st[0], 'state', '') == 'cancelled':
|
||||
logger.info(f"task cancelled mid-run: {task_id}")
|
||||
return {"status": "cancelled", "task_id": task_id}
|
||||
# 强制产出:第 6 轮起注入,打断「了解现状/反复验证」探索死循环,强制转向 write_file + deliver
|
||||
if turn >= _FORCE_PRODUCE_TURN:
|
||||
# 强制产出:第 6 轮起注入一次(2026-09-17 修复:原 turn>=5 每轮重复追加,
|
||||
# 30 轮预算下最多灌 25 条相同提示——上下文污染浪费 token 且挤压取证窗口。
|
||||
# 打断「了解现状/反复验证」探索死循环一次注入即够;真硬上限是 30 轮预算)。
|
||||
if turn == _FORCE_PRODUCE_TURN and not _force_produce_sent:
|
||||
msgs.append({"role": "user", "content": _FORCE_PRODUCE_HINT})
|
||||
_force_produce_sent = True
|
||||
try:
|
||||
resp = await asyncio.wait_for(
|
||||
llm_call_msgs_native(msgs, tools=tools_schema, model=model_name, temperature=0.4, org_id=org_id, project_id=project_id, session_id='task:%s' % task_id, timeout=_LLM_CLIENT_TIMEOUT),
|
||||
@ -3710,6 +3714,7 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
||||
from .llm_bridge import llm_call_msgs
|
||||
|
||||
decision = None
|
||||
_pm_soft_hint_sent = False
|
||||
for turn in range(5):
|
||||
# 心跳:PM 审核期间持续标记,进程崩溃后由 stale 回收重置;COMMIT 使其对其他连接可见
|
||||
await sor.sqlExe(
|
||||
@ -3725,12 +3730,17 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
||||
# auto-inject 兜底:最后两轮强制要求给出 review_* 决策,禁止再 tool_call,
|
||||
# 否则 deepseek 会一直读文件/git_status 耗尽 5 轮 → 审核超时 → 打回重跑 → 死循环。
|
||||
# 例外:允许 create_tasks(里程碑拆后续任务的「项目计划/任务分配」职责),派发后必须立即决策。
|
||||
if turn >= 3:
|
||||
# 收尾软提示(2026-09-17 修复:与 QC 循环同款缺陷——原 turn>=3 每轮重复
|
||||
# 追加禁令污染上下文 + 工具结果被替换成注入文本。PM read_file/git_status
|
||||
# 取证读到的是引擎指令而非真实内容,证据污染同款。改为只追加一次软提示,
|
||||
# 工具全程真实执行;轮次上限是硬约束,耗尽走下方「审核超时智能处理」。
|
||||
if turn == 3 and not _pm_soft_hint_sent:
|
||||
msgs.append({"role": "user", "content":
|
||||
"已检查足够信息。现在必须立即收尾:"
|
||||
"提示:剩余轮次不多(第 4/5 轮)。"
|
||||
"若本任务审批通过后需拆分为多个后续任务,现在用 create_tasks 一次性派发;"
|
||||
"随后必须立即输出 review_approve / review_reject / review_complete / review_rollback 之一,"
|
||||
"禁止再调用其它工具。"})
|
||||
"随后请尽快输出 review_approve / review_reject / review_complete / review_rollback 之一。"
|
||||
"你仍可调用取证工具,引擎不会伪造工具结果。"})
|
||||
_pm_soft_hint_sent = True
|
||||
try:
|
||||
# timeout 必传(2026-09-16 pbls QC 超时事故):不传则 payload 无
|
||||
# _timeout → inference 用供应商端点配置(百炼 120s)掐断上游,
|
||||
@ -3781,16 +3791,14 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
||||
elif tool == 'load_skill':
|
||||
result = await _load_skill_by_name(sor, project_id, 'pm', org_id, params.get('name', ''), params.get('file_path') or None)
|
||||
else:
|
||||
if turn >= 3:
|
||||
# 硬约束:最后两轮拒绝执行探索类工具(read_file/git_status/list_files/run_shell 等),
|
||||
# 强制 PM 立即输出决策——否则 deepseek 无视软提示持续检查、5 轮耗尽 → 审核超时。
|
||||
result = (f"已到最后收尾阶段(第 {turn + 1}/5 轮),拒绝执行探索类工具 {tool}。"
|
||||
f"请立即输出 review_approve / review_reject / review_complete / review_rollback 之一,不要再调用工具。")
|
||||
else:
|
||||
result = await _exec_agent_tool(tool, params, space_dir, {
|
||||
"project_id": project_id, "who": "agent.pm",
|
||||
"agent_id": agent_id or "", "task_id": task_id,
|
||||
"org_id": org_id or '0'})
|
||||
# 工具全程真实执行(2026-09-17 修复证据污染,与 QC 循环同款):
|
||||
# 原 turn>=3 把 result 替换成「拒绝执行探索类工具」注入文本——
|
||||
# PM 取证读到的是引擎指令而非文件内容。轮次硬上限=5 轮预算,
|
||||
# 耗尽走下方「审核超时智能处理」(有 create_tasks 视为认可,否则 rejected)。
|
||||
result = await _exec_agent_tool(tool, params, space_dir, {
|
||||
"project_id": project_id, "who": "agent.pm",
|
||||
"agent_id": agent_id or "", "task_id": task_id,
|
||||
"org_id": org_id or '0'})
|
||||
msgs.append({"role": "assistant", "content": raw})
|
||||
msgs.append({"role": "user", "content": f"工具 {tool} 结果:\n{result}"})
|
||||
else:
|
||||
@ -4107,14 +4115,25 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
|
||||
# QC 实际只有 1~2 次取证机会,而评分协议要求「逐项 read_file 核验、未读取一律判不过」
|
||||
# ——预算与协议结构性矛盾,QC 物理上无法完成核验 → 必然 reject → develop 做得再真
|
||||
# 也被退到上限(pbls M1a 四轮退回全在说「未能完成逐个 read_file 核验」)。
|
||||
_soft_hint_sent = False
|
||||
for turn in range(16):
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_tasks SET updated_at=NOW() WHERE id=${tid}$ AND state='qc_review'",
|
||||
{"tid": task_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if turn >= 12:
|
||||
# 收尾软提示(2026-09-17 修复:原 turn>=12 每轮重复追加禁令污染上下文,
|
||||
# 且配套硬切断把工具结果替换成注入文本 → QC read_file 取证读到的是
|
||||
# 「禁止调用工具」而非文件内容,证据链被污染、被迫盲判 reject,QC 退回
|
||||
# 意见②实锤)。改为:只追加一次软提示(不冒充工具结果),取证工具全程
|
||||
# 正常执行;硬上限是 16 轮预算本身——耗尽走 no-decision 诚实出口
|
||||
# (requeue→mark_failed→fault 冒泡人工),绝不静默 approve、绝不污染证据。
|
||||
if turn == 13 and not _soft_hint_sent:
|
||||
msgs.append({"role": "user", "content":
|
||||
"已检查足够信息。现在必须立即输出 review_approve 或 review_reject,禁止再调用其它工具。"})
|
||||
"提示:剩余取证轮次不多(第 14/16 轮)。请抓紧完成必要核验后输出 "
|
||||
"review_approve 或 review_reject(含 score/passed/total 与未过项清单)。"
|
||||
"你仍可调用 read_file 等取证工具,引擎不会伪造工具结果;若轮次耗尽"
|
||||
"仍无决策,任务会转人工而非默认放行——宁可如实退回,勿盲判。"})
|
||||
_soft_hint_sent = True
|
||||
try:
|
||||
# timeout 必传(2026-09-16 pbls QC 超时事故,与 PM 审核同款):
|
||||
# 不传 → inference 按端点配置 120s 掐断 + 客户端缺省 330s,QC
|
||||
@ -4149,12 +4168,11 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
|
||||
elif act.get('action') == 'tool_call':
|
||||
tool = act.get('tool', '')
|
||||
params = act.get('params', {})
|
||||
if turn >= 12 and tool != 'load_skill':
|
||||
# 硬约束(对齐 PM 循环成熟模式):收尾阶段拒绝执行探索类工具,
|
||||
# 强制 QC 立即输出决策——软提示实测会被无视、耗尽轮次。
|
||||
result = (f"已到最后收尾阶段(第 {turn + 1}/16 轮),拒绝执行探索类工具 {tool}。"
|
||||
f"请立即输出 review_approve 或 review_reject(含 score/passed/total 与未过项清单),不要再调用工具。")
|
||||
elif tool == 'load_skill':
|
||||
# 工具全程真实执行(2026-09-17 修复证据污染):原 turn>=12 把 result
|
||||
# 替换成「拒绝执行探索类工具」注入文本,QC read_file 拿到的是引擎指令
|
||||
# 而非文件内容 → 证据链污染 → 被迫盲判。现在取证工具任何时候都真跑,
|
||||
# 收尾约束交给软提示 + 16 轮预算硬上限(耗尽转人工,不默认放行)。
|
||||
if tool == 'load_skill':
|
||||
result = await _load_skill_by_name(sor, project_id, 'qc', org_id, params.get('name', ''), params.get('file_path') or None)
|
||||
else:
|
||||
result = await _exec_agent_tool(tool, params, space_dir, capability_ctx)
|
||||
@ -4366,14 +4384,20 @@ async def retrospective_run(project_id, agent_id=None, model_name=None):
|
||||
from .llm_bridge import llm_call_msgs
|
||||
deliverable = None
|
||||
proposals_made = 0
|
||||
_rt_soft_hint_sent = False
|
||||
for turn in range(10):
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_tasks SET updated_at=NOW() WHERE id=${tid}$ AND state='running'",
|
||||
{"tid": task_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if turn >= 7:
|
||||
# 收尾软提示(2026-09-17 与 QC/PM 同款修复:原 turn>=7 每轮重复追加污染上下文;
|
||||
# 工具本就真实执行无证据污染。只追加一次,轮次硬上限=10 轮预算,
|
||||
# 耗尽走下方「轮次耗尽未产出交付件」mark_failed 诚实出口)
|
||||
if turn == 7 and not _rt_soft_hint_sent:
|
||||
msgs.append({"role": "user", "content":
|
||||
"已执行足够轮次。现在必须立即收尾:write_file 写复盘报告(若未写),然后输出 deliver 提交,禁止再调其它工具。"})
|
||||
"提示:剩余轮次不多(第 8/10 轮)。请尽快收尾:write_file 写复盘报告(若未写),"
|
||||
"然后输出 deliver 提交。你仍可调用必要工具,引擎不会伪造工具结果。"})
|
||||
_rt_soft_hint_sent = True
|
||||
try:
|
||||
raw = await asyncio.wait_for(
|
||||
# timeout 必传(2026-09-16 pbls 同款预算错位第三处:复盘)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user