IQuest-Q1 工具调用返回解不出时,先别急着认定是解析代码写错,也别默认对话模板一定没问题。模板没对齐时,返回里可能根本没有工具段落或角色标记;输出被长度上限截断时,末尾常见未闭合括号或缺少结束标记;解析层写错时,同一条提示往往稳定报错。先把失败请求的原始返回原样存下来,再按模板、截断、解析三条线分开判断。
先保留失败请求的请求体、原始返回和耗时,再用同一提示重复跑几次。若每次工具名和参数一致但解析仍报错,优先查解析层;若返回末尾缺结束标记或括号未闭合,先查长度限制和上下文长度;若返回本身缺少工具段落,再回模板文件核对角色标记与工具列表位置。未拿到原始返回前,不建议同时改模板和解析。
把失败请求的原始返回原样存下来
很多排查卡住,是因为解析报错后只看到异常栈,看不到模型到底返回了什么。建议在调用入口处加一层落盘,把请求体、原始返回、耗时和模板版本写进同一行记录。关键点是原始返回不要先 json.loads 再 json.dumps,也不要 strip 后再存;否则键序、空格、转义和末尾字符都可能被改写,后面无法判断是不是截断。
import json
import time
def save_failure(case_id, request_body, raw_text, elapsed_ms, template_version):
record = {
'case_id': case_id,
'request_body': request_body,
'raw_text': raw_text,
'elapsed_ms': elapsed_ms,
'template_version': template_version,
}
with open('tool_call_failures.jsonl', 'a', encoding='utf-8') as f:
f.write(json.dumps(record, ensure_ascii=False) + chr(10))
替换项:request_body 换成你实际发给模型的 messages 或请求体,raw_text 换成响应对象里的原始文本,template_version 换成你当前加载的模板文件名或哈希。验证方式:打开 tool_call_failures.jsonl,看 raw_text 字段里是否仍保留尖括号、换行和末尾字符。风险边界:日志可能含用户数据,需要配合脱敏和轮转;如果原始返回是流式拼接的,要确认拼接顺序没有被改写。
对照模板文件检查工具段落是否被漏发
如果原始返回里根本没有工具名、没有工具参数、甚至角色标记都不完整,那问题更可能在提示结构而不是解析层。建议把最终发给模型的完整请求体打印出来,逐项核对,而不是只读模板文件。常见漏拼点如下:
- system、user、assistant 角色标记是否成对出现,多轮拼接时有没有把工具返回当成新的 user 消息。
- 工具列表是否放在模板约定的位置,例如 system 段落末尾或独立 tools 字段;有些接入方式要求 tools 放在请求参数里,而不是塞进 messages。
- 工具名和参数 schema 是否被模板引擎转义,例如引号变成 HTML 实体。
- 结束标记是否漏拼,例如 </tool_call>、<|end|>、eot 等;结束标记缺失时,模型可能继续补全而不是给出闭合结构。
验证方式:在调用前打印最终请求体,搜索工具名和结束标记是否同时存在。如果模板文件里写了工具段落,但请求体里没有,通常是拼装顺序或字段层级的问题。需要结合你使用的 IQuest-Q1 接入层确认,不同框架对 tools 字段的位置要求可能不同。
在解析层打印截断位置
解析失败时,先区分是格式不合法还是输出被长度上限切断。不要只看异常信息,建议在解析前打印返回长度和末尾若干字符,并检查结束标记与括号闭合。
raw = resp_text
print('len=', len(raw))
print('tail=', repr(raw[-160:]))
print('has_end=', '</tool_call>' in raw)
print('brace_ok=', raw.rstrip().endswith('}'))
如果末尾是半截工具名、未闭合的 { 或 [,并且缺少 </tool_call> 这类结束标记,通常优先怀疑 max_tokens 或其他长度限制把输出截断了,而不是解析器写错。反过来,如果长度明显小于配置上限,末尾也有闭合结束标记,但解析器仍报错,就要检查返回是否符合解析器预期的格式,例如工具段落外是否多了 Markdown 围栏、是否返回了多个工具调用。判断未闭合括号时,可以从末尾往前找最后一个开括号,看有没有对应闭括号;也可以把 raw 复制到本地执行 json.loads,看异常位置是否落在末尾附近。
用同一条提示重跑多次看是否稳定复现
同一提示只跑一次,很难区分偶发漂移和必然错误。建议保持请求体不变,重复跑 5 到 10 次,记录每次是否解析成功、工具名是什么、参数是否与首次一致。命中率口径可以这样定:解析成功次数除以总次数;工具名一致次数除以总次数;参数 JSON 完全一致次数除以总次数。不要只看一个笼统比例,要标明每次是否给出相同工具名与相同参数。
- run_id、请求体哈希或 case_id。
- 原始返回长度、末尾字符、是否包含结束标记。
- 解析是否成功、失败原因。
- 工具名、参数 JSON、是否与首次一致。
如果每次都在同一位置失败,且工具名和参数相同,优先查模板或解析逻辑;如果有时成功、有时失败,且失败时返回长度靠近上限,优先缩上下文或调整长度限制。需要结合环境确认:服务端缓存、并发、采样参数都可能影响重复运行结果。
按复现结论决定改模板、改解析还是缩上下文
排查到最后,一次只改一处,否则无法归因。三类原因对应的修法和验证方式如下:
- 模板漏发:补齐角色标记、工具列表位置和结束标记。验证方式是重新打印最终请求体,确认工具名和结束标记都在;再用同一条提示跑一次,确认原始返回里出现完整工具段落。
- 输出截断:提高 max_tokens,或缩短上下文和工具描述,必要时把一次多工具调用拆成多轮。验证方式是观察原始返回长度和末尾是否出现闭合结束标记,解析层能拿到完整结构。
- 解析不健壮:不要直接对整个返回做 json.loads,先提取工具段落,再对工具参数做解析,兼容前后空格、Markdown 围栏和多个工具调用。验证方式是用历史失败样本回放,确认解析层能提取出工具名和参数。
改完后再跑同一条提示,确认原来的错误消失。如果错误仍在,回到原始返回重新判断,而不是继续叠加改动。边界是:IQuest-Q1 的返回格式和接入层配置需要以你本地打印出的请求体、原始返回和模板文件为准,通用经验只能作为排查顺序。