94 lines
4.1 KiB
Python
Raw 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 万 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