fix(agent): 工具结果回填上限——result_cap.py(12000字符默认+params可调+显式截断告知+缩小范围指引);v2两处/v1一处回填点全接;根治商机产线实测:8次数据工具全文回填~78K token超context_limit→压缩摘要自身超时3连败→会话零产出(对照实验:同模型小上下文3.4s正常)
This commit is contained in:
parent
2c35abfdab
commit
1faa1c1d2f
@ -2520,7 +2520,13 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
|||||||
break
|
break
|
||||||
if tool == "write_file" and params.get("path"):
|
if tool == "write_file" and params.get("path"):
|
||||||
written_files.append(os.path.join(space_dir, params["path"]))
|
written_files.append(os.path.join(space_dir, params["path"]))
|
||||||
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": str(result)})
|
# 回填上限(2026-09-11,与 v2 同款):数据类工具全文回填会撑爆上下文
|
||||||
|
# → 压缩摘要自身超时 → 任务失败。截断显式告知 + 缩小范围指引。
|
||||||
|
from .result_cap import cap_tool_result, resolve_max_chars
|
||||||
|
_capped, _trunc = cap_tool_result(result, await resolve_max_chars(sor))
|
||||||
|
if _trunc:
|
||||||
|
logger.info(f"role_agent tool_result truncated: {tool}")
|
||||||
|
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": _capped})
|
||||||
logger.info(f"role_agent tool_call: {tool} -> {str(result)[:100]}")
|
logger.info(f"role_agent tool_call: {tool} -> {str(result)[:100]}")
|
||||||
if deliverable or ask_question:
|
if deliverable or ask_question:
|
||||||
break
|
break
|
||||||
|
|||||||
@ -236,7 +236,12 @@ class AgentExecutor:
|
|||||||
if not result.startswith("未知工具") and not result.startswith("ERROR"):
|
if not result.startswith("未知工具") and not result.startswith("ERROR"):
|
||||||
self._tool_call_count += 1
|
self._tool_call_count += 1
|
||||||
yield json.dumps({"type": "tool_result", "tool": pending_tool, "result": result[:500]}, ensure_ascii=False) + "\n"
|
yield json.dumps({"type": "tool_result", "tool": pending_tool, "result": result[:500]}, ensure_ascii=False) + "\n"
|
||||||
self._msgs.append({"role": "tool", "tool_call_id": fake_id, "content": str(result)})
|
# 回填上限(2026-09-11):工具结果全文回填会撑爆上下文(商机产线实测
|
||||||
|
# 8 次数据工具 ~78K token → 压缩摘要自身超时 → 会话零产出)。
|
||||||
|
# 截断显式告知 + 给缩小范围指引,让 LLM 自己改策略。
|
||||||
|
from .result_cap import cap_tool_result, resolve_max_chars
|
||||||
|
_capped, _ = cap_tool_result(result, await resolve_max_chars())
|
||||||
|
self._msgs.append({"role": "tool", "tool_call_id": fake_id, "content": _capped})
|
||||||
# 不 return,继续进入 Tool Loop,让 LLM 基于工具结果继续后续步骤
|
# 不 return,继续进入 Tool Loop,让 LLM 基于工具结果继续后续步骤
|
||||||
|
|
||||||
# Step 5: Tool Loop
|
# Step 5: Tool Loop
|
||||||
@ -339,10 +344,15 @@ class AgentExecutor:
|
|||||||
}, ensure_ascii=False) + "\n"
|
}, ensure_ascii=False) + "\n"
|
||||||
|
|
||||||
# 原生回填:role=tool + tool_call_id
|
# 原生回填:role=tool + tool_call_id
|
||||||
|
# 回填上限:防数据类工具全文回填撑爆上下文(见 result_cap 模块 docstring)
|
||||||
|
from .result_cap import cap_tool_result, resolve_max_chars
|
||||||
|
_capped, _trunc = cap_tool_result(result, await resolve_max_chars())
|
||||||
|
if _trunc:
|
||||||
|
logger.info(f"tool_result truncated before refill: {tool_name}")
|
||||||
self._msgs.append({
|
self._msgs.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": tc.get("id", "") if isinstance(tc, dict) else "",
|
"tool_call_id": tc.get("id", "") if isinstance(tc, dict) else "",
|
||||||
"content": str(result),
|
"content": _capped,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
93
pipeline_service/result_cap.py
Normal file
93
pipeline_service/result_cap.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""工具结果回填上限(通用,v1 角色 agent + v2 会话 agent 共用)。
|
||||||
|
|
||||||
|
问题根因(2026-09-11 商机产线实测会话失败):
|
||||||
|
工具结果回填给模型时是 `str(result)` **全文不截断**(仅 UI 显示截到 500 字),
|
||||||
|
数据类工具一次返回 300 条原始记录 ≈ 2 万字符 ≈ 1 万 token;agent 连调 8 次
|
||||||
|
→ 上下文 ~78K token 超过 context_limit(64K) → 压缩摘要调用自身也超时(重试 3 次
|
||||||
|
全败)→ 整轮会话失败、零产出。对照实验:同模型小上下文 3.4s 正常返回,
|
||||||
|
证明不是上游故障而是上下文体积。
|
||||||
|
|
||||||
|
修法:回填前按字符上限截断,**显式告知**(沿用 file_read.py 的「绝不静默截断」
|
||||||
|
铁律)——告诉模型全文多大、只给了多少、以及怎么缩小范围(减小 limit /
|
||||||
|
改用聚合排名类工具),让它自己改策略而不是反复拉全量。
|
||||||
|
|
||||||
|
上限走 appbase params 表(tool_result_max_chars)+ 常量兜底,禁硬编码。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger("pipeline.result_cap")
|
||||||
|
|
||||||
|
# 兜底上限(params 表无配置时用)。12000 字符 ≈ 6000 token:
|
||||||
|
# 够放一份结构化聚合结果或几十条明细,放 300 条原始记录则会被截断并提示。
|
||||||
|
DEFAULT_MAX_CHARS = 12000
|
||||||
|
|
||||||
|
_PARAM_KEY = "tool_result_max_chars"
|
||||||
|
# 进程内缓存(None = 未解析,惰性读 params;invalidate_cache() 清除)
|
||||||
|
_cached_max: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_max_chars(sor=None):
|
||||||
|
"""取回填上限(params 表优先,失败/未配置用常量兜底,进程内缓存)。
|
||||||
|
|
||||||
|
sor=None 时自开短连接读取(调用点如 agent_loop_v2.run() 的工具回填处
|
||||||
|
没有现成 DB context;结果缓存后不再重复开连接)。读失败一律兜底常量,
|
||||||
|
绝不因为取参数失败而打断 agent 主循环。
|
||||||
|
"""
|
||||||
|
global _cached_max
|
||||||
|
if _cached_max is not None:
|
||||||
|
return _cached_max
|
||||||
|
val = DEFAULT_MAX_CHARS
|
||||||
|
try:
|
||||||
|
from .workspace import get_param
|
||||||
|
if sor is not None:
|
||||||
|
raw = await get_param(sor, _PARAM_KEY, "")
|
||||||
|
else:
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
db = DBPools()
|
||||||
|
if not db.databases:
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
cfg = getConfig()
|
||||||
|
if cfg and cfg.databases:
|
||||||
|
db.databases = cfg.databases
|
||||||
|
async with db.sqlorContext("pipeline") as _sor:
|
||||||
|
raw = await get_param(_sor, _PARAM_KEY, "")
|
||||||
|
await _sor.sqlExe("COMMIT", {})
|
||||||
|
if raw not in (None, ""):
|
||||||
|
val = max(1000, int(float(raw)))
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("resolve_max_chars fallback to default: %s", str(e)[:120])
|
||||||
|
val = DEFAULT_MAX_CHARS
|
||||||
|
_cached_max = val
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def cap_tool_result(result, max_chars=None):
|
||||||
|
"""截断工具结果(同步;max_chars=None 用常量兜底)。
|
||||||
|
|
||||||
|
返回 (text, truncated)。截断时末尾附显式告知 + 可行动的缩小范围指引。
|
||||||
|
非字符串结果先 str() 归一(dict/list 也走同一路径)。
|
||||||
|
"""
|
||||||
|
text = result if isinstance(result, str) else str(result)
|
||||||
|
limit = int(max_chars) if max_chars else DEFAULT_MAX_CHARS
|
||||||
|
total = len(text)
|
||||||
|
if total <= limit:
|
||||||
|
return text, False
|
||||||
|
notice = (
|
||||||
|
"\n\n⚠️【结果过长已截断】全文 %d 字符,仅回填前 %d 字符(上限可用 params.%s 调整)。\n"
|
||||||
|
"不要重复调用同一工具拉全量——请缩小范围后重试:\n"
|
||||||
|
"- 减小 limit(如 limit=10~20)或缩小 days/时间窗口;\n"
|
||||||
|
"- 优先用聚合/排名类工具(如 hot_demands/hot_software/category 汇总)"
|
||||||
|
"直接拿统计值,而不是拉明细自己数;\n"
|
||||||
|
"- 明细需要落盘时用 write_file 写到项目工作空间,不要全量读进对话。"
|
||||||
|
% (total, limit, _PARAM_KEY)
|
||||||
|
)
|
||||||
|
return text[:limit] + notice, True
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_cache():
|
||||||
|
"""params 变更后让缓存失效(部署/调参后无需重启进程)。"""
|
||||||
|
global _cached_max
|
||||||
|
_cached_max = None
|
||||||
Loading…
x
Reference in New Issue
Block a user