149 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""工具结果回填上限通用v1 角色 agent + v2 会话 agent 共用)。
问题根因2026-09-11 商机产线实测会话失败):
工具结果回填给模型时是 `str(result)` **全文不截断**(仅 UI 显示截到 500 字),
数据类工具一次返回 300 条原始记录 ≈ 2 万字符 ≈ 1 万 tokenagent 连调 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 = 未解析,惰性读 paramsinvalidate_cache() 清除)
_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):
"""取回填上限params 表优先,失败/未配置用常量兜底,进程内缓存)。
sor=None 时自开短连接读取(调用点如 agent_loop_v2.run() 的工具回填处
没有现成 DB context结果缓存后不再重复开连接。读失败一律兜底常量
绝不因为取参数失败而打断 agent 主循环。
"""
global _cached_max
if _cached_max is not None:
return _cached_max
_cached_max = await _resolve_param_int(_PARAM_KEY, DEFAULT_MAX_CHARS, 1000, "_cached_max", sor)
return _cached_max
async def resolve_display_max_chars():
"""取 UI 显示层上限params.tool_result_display_max_chars默认 50000"""
global _cached_display_max
if _cached_display_max is not None:
return _cached_display_max
_cached_display_max = await _resolve_param_int(
_PARAM_DISPLAY_KEY, DEFAULT_DISPLAY_MAX_CHARS, 500, "_cached_display_max")
return _cached_display_max
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 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():
"""params 变更后让缓存失效(部署/调参后无需重启进程)。"""
global _cached_max, _cached_display_max
_cached_max = None
_cached_display_max = None