# -*- 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