feat(agent): 工具结果UI显示层取消500字硬截——params可配上限(默认50000≈完整显示)+超限显式告知(2026-09-15用户指令:显示完整信息)。显示层与回填层(12000防爆门禁)分离,resolve_max_chars提取共享_resolve_param_int,新增cap_display_result/resolve_display_max_chars(params键tool_result_display_max_chars)
This commit is contained in:
parent
6dac18e1b1
commit
715be0b387
@ -266,11 +266,14 @@ class AgentExecutor:
|
|||||||
result = await self._execute_tool(pending_tool, pending_params)
|
result = await self._execute_tool(pending_tool, pending_params)
|
||||||
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"
|
# 显示层上限(2026-09-15 用户要求完整显示,替代旧硬截 [:500]):
|
||||||
|
# 默认 50000 字符 = 正常工具输出完整显示;超限显式告知(绝不静默截断)。
|
||||||
|
from .result_cap import cap_tool_result, cap_display_result, resolve_max_chars, resolve_display_max_chars
|
||||||
|
yield json.dumps({"type": "tool_result", "tool": pending_tool,
|
||||||
|
"result": cap_display_result(result, await resolve_display_max_chars())}, ensure_ascii=False) + "\n"
|
||||||
# 回填上限(2026-09-11):工具结果全文回填会撑爆上下文(商机产线实测
|
# 回填上限(2026-09-11):工具结果全文回填会撑爆上下文(商机产线实测
|
||||||
# 8 次数据工具 ~78K token → 压缩摘要自身超时 → 会话零产出)。
|
# 8 次数据工具 ~78K token → 压缩摘要自身超时 → 会话零产出)。
|
||||||
# 截断显式告知 + 给缩小范围指引,让 LLM 自己改策略。
|
# 截断显式告知 + 给缩小范围指引,让 LLM 自己改策略。
|
||||||
from .result_cap import cap_tool_result, resolve_max_chars
|
|
||||||
_capped, _ = cap_tool_result(result, await resolve_max_chars())
|
_capped, _ = cap_tool_result(result, await resolve_max_chars())
|
||||||
self._msgs.append({"role": "tool", "tool_call_id": fake_id, "content": _capped})
|
self._msgs.append({"role": "tool", "tool_call_id": fake_id, "content": _capped})
|
||||||
# 不 return,继续进入 Tool Loop,让 LLM 基于工具结果继续后续步骤
|
# 不 return,继续进入 Tool Loop,让 LLM 基于工具结果继续后续步骤
|
||||||
@ -411,8 +414,11 @@ class AgentExecutor:
|
|||||||
await self._save_turn(user_input, f"[提问] {question}")
|
await self._save_turn(user_input, f"[提问] {question}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 显示层上限(同 Step 4.5,默认 50000 = 完整显示;超限显式告知)
|
||||||
|
from .result_cap import cap_display_result, resolve_display_max_chars
|
||||||
yield json.dumps({
|
yield json.dumps({
|
||||||
"type": "tool_result", "tool": tool_name, "result": result[:500],
|
"type": "tool_result", "tool": tool_name,
|
||||||
|
"result": cap_display_result(result, await resolve_display_max_chars()),
|
||||||
}, ensure_ascii=False) + "\n"
|
}, ensure_ascii=False) + "\n"
|
||||||
|
|
||||||
# 原生回填:role=tool + tool_call_id
|
# 原生回填:role=tool + tool_call_id
|
||||||
@ -515,9 +521,11 @@ class AgentExecutor:
|
|||||||
await self._save_turn(user_input, f"[提问] {question}")
|
await self._save_turn(user_input, f"[提问] {question}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 显示层上限(同 native 路径,默认 50000 = 完整显示;超限显式告知)
|
||||||
|
from .result_cap import cap_display_result, resolve_display_max_chars
|
||||||
yield json.dumps({
|
yield json.dumps({
|
||||||
"type": "tool_result", "tool": tool_name,
|
"type": "tool_result", "tool": tool_name,
|
||||||
"result": result[:500],
|
"result": cap_display_result(result, await resolve_display_max_chars()),
|
||||||
}, ensure_ascii=False) + "\n"
|
}, ensure_ascii=False) + "\n"
|
||||||
|
|
||||||
# 反馈给 LLM(关键:不存原始 tool_call JSON)
|
# 反馈给 LLM(关键:不存原始 tool_call JSON)
|
||||||
|
|||||||
@ -28,6 +28,51 @@ _PARAM_KEY = "tool_result_max_chars"
|
|||||||
# 进程内缓存(None = 未解析,惰性读 params;invalidate_cache() 清除)
|
# 进程内缓存(None = 未解析,惰性读 params;invalidate_cache() 清除)
|
||||||
_cached_max: Optional[int] = None
|
_cached_max: Optional[int] = None
|
||||||
|
|
||||||
|
# ── UI 显示层上限(2026-09-15,用户要求「显示完整信息,不要裁剪」)──
|
||||||
|
# 历史行为:agent_loop_v2 yield tool_result 事件时硬截 result[:500] 且无提示
|
||||||
|
# (静默截断,违反 file_read 的「绝不静默截断」纪律),前端看到半截输出。
|
||||||
|
# 显示层与回填层是两条独立通道:显示只影响人眼,不进模型上下文,
|
||||||
|
# 因此上限可以远高于回填上限。默认 50000 字符覆盖所有正常工具输出
|
||||||
|
# (load_skill 全文 / diagnose_project / read_file 分页 ≈ 数千~3万字符),
|
||||||
|
# 等于完整显示;只兜底 run_command 输出数 MB 的病态情况(防 NDJSON
|
||||||
|
# 流 + 前端 MdWidget 渲染卡死)。要完全不限:params 表设大值即可,无需改码。
|
||||||
|
DEFAULT_DISPLAY_MAX_CHARS = 50000
|
||||||
|
_PARAM_DISPLAY_KEY = "tool_result_display_max_chars"
|
||||||
|
_cached_display_max: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def _resolve_param_int(param_key: str, default: int, floor: int, cache_attr: str, sor=None) -> int:
|
||||||
|
"""从 params 表读整型配置(进程内缓存;读失败一律兜底常量,绝不打断主循环)。
|
||||||
|
|
||||||
|
sor 非 None 时复用调用方现成连接(v1 agent_loop 路径);否则自开短连接。
|
||||||
|
"""
|
||||||
|
cached = globals().get(cache_attr)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
val = default
|
||||||
|
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(floor, int(float(raw)))
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("resolve %s fallback to default: %s", param_key, str(e)[:120])
|
||||||
|
val = default
|
||||||
|
globals()[cache_attr] = val
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
async def resolve_max_chars(sor=None):
|
async def resolve_max_chars(sor=None):
|
||||||
"""取回填上限(params 表优先,失败/未配置用常量兜底,进程内缓存)。
|
"""取回填上限(params 表优先,失败/未配置用常量兜底,进程内缓存)。
|
||||||
@ -39,29 +84,18 @@ async def resolve_max_chars(sor=None):
|
|||||||
global _cached_max
|
global _cached_max
|
||||||
if _cached_max is not None:
|
if _cached_max is not None:
|
||||||
return _cached_max
|
return _cached_max
|
||||||
val = DEFAULT_MAX_CHARS
|
_cached_max = await _resolve_param_int(_PARAM_KEY, DEFAULT_MAX_CHARS, 1000, "_cached_max", sor)
|
||||||
try:
|
return _cached_max
|
||||||
from .workspace import get_param
|
|
||||||
if sor is not None:
|
|
||||||
raw = await get_param(sor, _PARAM_KEY, "")
|
async def resolve_display_max_chars():
|
||||||
else:
|
"""取 UI 显示层上限(params.tool_result_display_max_chars,默认 50000)。"""
|
||||||
from sqlor.dbpools import DBPools
|
global _cached_display_max
|
||||||
db = DBPools()
|
if _cached_display_max is not None:
|
||||||
if not db.databases:
|
return _cached_display_max
|
||||||
from appPublic.jsonConfig import getConfig
|
_cached_display_max = await _resolve_param_int(
|
||||||
cfg = getConfig()
|
_PARAM_DISPLAY_KEY, DEFAULT_DISPLAY_MAX_CHARS, 500, "_cached_display_max")
|
||||||
if cfg and cfg.databases:
|
return _cached_display_max
|
||||||
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):
|
def cap_tool_result(result, max_chars=None):
|
||||||
@ -87,7 +121,28 @@ def cap_tool_result(result, max_chars=None):
|
|||||||
return text[:limit] + notice, True
|
return text[:limit] + notice, True
|
||||||
|
|
||||||
|
|
||||||
|
def cap_display_result(result, max_chars=None):
|
||||||
|
"""UI 显示层截断(同步;max_chars=None 用显示层常量兜底)。
|
||||||
|
|
||||||
|
与 cap_tool_result(模型回填层)是两条独立通道:这里只影响前端展示,
|
||||||
|
不进模型上下文。截断时显式告知(绝不静默截断),正常工具输出
|
||||||
|
(<50000 字符)原样完整显示。
|
||||||
|
"""
|
||||||
|
text = result if isinstance(result, str) else str(result)
|
||||||
|
limit = int(max_chars) if max_chars else DEFAULT_DISPLAY_MAX_CHARS
|
||||||
|
total = len(text)
|
||||||
|
if total <= limit:
|
||||||
|
return text
|
||||||
|
notice = (
|
||||||
|
"\n\n…(输出过长,界面仅显示前 %d 字符,全文共 %d 字符。"
|
||||||
|
"模型收到的内容不受此显示限制影响;上限可用 params.%s 调整)"
|
||||||
|
% (limit, total, _PARAM_DISPLAY_KEY)
|
||||||
|
)
|
||||||
|
return text[:limit] + notice
|
||||||
|
|
||||||
|
|
||||||
def invalidate_cache():
|
def invalidate_cache():
|
||||||
"""params 变更后让缓存失效(部署/调参后无需重启进程)。"""
|
"""params 变更后让缓存失效(部署/调参后无需重启进程)。"""
|
||||||
global _cached_max
|
global _cached_max, _cached_display_max
|
||||||
_cached_max = None
|
_cached_max = None
|
||||||
|
_cached_display_max = None
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user