fix(agent): 会话健壮性三修——①收尾门禁(_WRAP_UP_RESERVE=3,轮次将尽注入立即收尾指令,根治长任务撞穿max_turns静默退出只给中间句);②utility调用(压缩摘要/技能选择)显式钉model+短超时60s快速失败走兜底(根治上游抖动窗口内连烧15分钟卡死整轮);③主调用会话级重试(瞬时Timeout/ClientError重试1次+透出进度,不再单次失败即终止会话丢整轮工作)
This commit is contained in:
parent
1faa1c1d2f
commit
2e05029568
@ -87,6 +87,18 @@ class AgentExecutor:
|
|||||||
4. 会话持久化
|
4. 会话持久化
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 收尾预留轮次(2026-09-11):轮次预算剩这么多轮时注入「立即收尾」指令,
|
||||||
|
# 强制 LLM 停止调工具、基于已有材料输出最终结论。根因见 run() 内 5-pre0 段注释
|
||||||
|
#(v1 有 _FORCE_PRODUCE_HINT、v2 此前缺等价机制 → 长任务撞穿 max_turns 后
|
||||||
|
# 静默退出,用户只拿到中间进度句)。
|
||||||
|
_WRAP_UP_RESERVE = 3
|
||||||
|
|
||||||
|
# 辅助调用(压缩摘要/技能选择/意图分类)超时上限(2026-09-11)。
|
||||||
|
# 这些是「可降级」调用:失败有兜底(摘要退化为截断文本、技能选择退化为目录层)。
|
||||||
|
# 给短超时让它们快速失败走兜底,而不是占用端点默认 300s × 3 重试把整轮卡死
|
||||||
|
#(实测上游抖动窗口内 utility 调用连烧 15 分钟才放弃,会话全程无响应)。
|
||||||
|
_UTILITY_TIMEOUT = 60
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config, # AgentConfig (from pipeline_core)
|
config, # AgentConfig (from pipeline_core)
|
||||||
@ -253,6 +265,26 @@ class AgentExecutor:
|
|||||||
for turn in range(max_turns):
|
for turn in range(max_turns):
|
||||||
self._turn_count = turn + 1
|
self._turn_count = turn + 1
|
||||||
|
|
||||||
|
# 5-pre0. 收尾门禁(2026-09-11 商机产线实测教训):轮次预算接近耗尽时
|
||||||
|
# 注入「立即收尾」指令,强制 LLM 用已有材料给出最终结论。
|
||||||
|
# 根因:v1 角色 agent 有 _FORCE_PRODUCE_HINT 打断探索循环,v2 会话 agent
|
||||||
|
# 此前没有等价机制——多工具长任务(如商机调研→功能点估算→成本→总结)
|
||||||
|
# 会跑满 max_turns 后静默退出,用户只看到中间进度句(「功能点已算出…」),
|
||||||
|
# 拿不到要求的总结。收尾轮不再允许调工具,只准产出最终回答。
|
||||||
|
_wrap_turn = max_turns - self._WRAP_UP_RESERVE
|
||||||
|
if turn == _wrap_turn:
|
||||||
|
self._msgs.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
"⚠️【收尾指令】你的对话轮次即将用尽(仅剩 %d 轮)。"
|
||||||
|
"立即停止调用任何工具,基于已经获取的材料直接给出**完整的最终回答**:"
|
||||||
|
"把用户要求的每一项都答到(数据/结论/建议/总结),"
|
||||||
|
"信息不全的部分如实标注「数据未覆盖」并给出你的专业判断,"
|
||||||
|
"不要再做探索、核对或补数。现在就输出最终结论。" % (max_turns - turn)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
logger.info(f"wrap-up hint injected at turn {turn+1}/{max_turns}")
|
||||||
|
|
||||||
# 5-pre. 子 agent 运行时钩子(2026-09-10):心跳 + steer 消费 + stop 检测。
|
# 5-pre. 子 agent 运行时钩子(2026-09-10):心跳 + steer 消费 + stop 检测。
|
||||||
# 后台委派的子 executor 带 _subagent_id,每轮 tool-loop 边界:
|
# 后台委派的子 executor 带 _subagent_id,每轮 tool-loop 边界:
|
||||||
# - heartbeat 刷新 meta.updated_at(stale 判活依据)
|
# - heartbeat 刷新 meta.updated_at(stale 判活依据)
|
||||||
@ -284,11 +316,32 @@ class AgentExecutor:
|
|||||||
# 5b. LLM 调用(native function calling,返回 dict)
|
# 5b. LLM 调用(native function calling,返回 dict)
|
||||||
# 模型不可用等配置错误必须真实报给用户(error 事件),
|
# 模型不可用等配置错误必须真实报给用户(error 事件),
|
||||||
# 绝不能让异常打断流——前端会停在"思考中..."干等。
|
# 绝不能让异常打断流——前端会停在"思考中..."干等。
|
||||||
try:
|
# 会话级重试(2026-09-11):上游瞬时抖动(TimeoutError/ClientError)时
|
||||||
resp = await self._call_llm()
|
# 重试一次再放弃。实测上游存在分钟级抖动窗口(同模型同上下文直调成功、
|
||||||
except Exception as e:
|
# 产线调用 TimeoutError 3 连败),单次失败即终止会话代价过高(整轮工作丢失)。
|
||||||
logger.error(f"run: LLM 调用失败 turn={turn+1}: {e}")
|
# 重试前向用户透出进度,避免"卡死"观感。
|
||||||
yield json.dumps({"type": "error", "message": str(e)}, ensure_ascii=False) + "\n"
|
resp = None
|
||||||
|
_last_err = None
|
||||||
|
for _attempt in range(2):
|
||||||
|
try:
|
||||||
|
resp = await self._call_llm()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
_last_err = e
|
||||||
|
_transient = any(k in type(e).__name__ or k in str(e)
|
||||||
|
for k in ("Timeout", "ClientError", "ServerDisconnected",
|
||||||
|
"ConnectionReset"))
|
||||||
|
if not _transient or _attempt >= 1:
|
||||||
|
break
|
||||||
|
logger.warning(f"run: LLM 瞬时失败 turn={turn+1} 重试: {e}")
|
||||||
|
yield json.dumps({
|
||||||
|
"type": "progress",
|
||||||
|
"message": "模型上游瞬时超时,正在重试…\n",
|
||||||
|
}, ensure_ascii=False) + "\n"
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
if resp is None:
|
||||||
|
logger.error(f"run: LLM 调用失败 turn={turn+1}: {_last_err}")
|
||||||
|
yield json.dumps({"type": "error", "message": str(_last_err)}, ensure_ascii=False) + "\n"
|
||||||
return
|
return
|
||||||
|
|
||||||
# 5c. 原生 function calling:优先处理 tool_calls
|
# 5c. 原生 function calling:优先处理 tool_calls
|
||||||
@ -664,6 +717,7 @@ class AgentExecutor:
|
|||||||
temperature=0,
|
temperature=0,
|
||||||
org_id=self.org_id,
|
org_id=self.org_id,
|
||||||
purpose='utility',
|
purpose='utility',
|
||||||
|
timeout=self._UTILITY_TIMEOUT,
|
||||||
)
|
)
|
||||||
m = _re.search(r"\[[^\]]*\]", content or "")
|
m = _re.search(r"\[[^\]]*\]", content or "")
|
||||||
if m:
|
if m:
|
||||||
@ -2133,8 +2187,10 @@ class AgentExecutor:
|
|||||||
summary = await llm_call(
|
summary = await llm_call(
|
||||||
f"请用3-5句话总结以下对话的关键信息:\n\n{text[:4000]}",
|
f"请用3-5句话总结以下对话的关键信息:\n\n{text[:4000]}",
|
||||||
temperature=0.1,
|
temperature=0.1,
|
||||||
|
model=self.model_name,
|
||||||
org_id=self.org_id,
|
org_id=self.org_id,
|
||||||
purpose='utility',
|
purpose='utility',
|
||||||
|
timeout=self._UTILITY_TIMEOUT,
|
||||||
)
|
)
|
||||||
return summary[:500]
|
return summary[:500]
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user