IQuest-Q1 要不要继续投入,可以先看两件互不依赖的事:单轮推理能不能返回可用文本,带工具的请求返回能不能被程序解析。前者用最小输入跑一次并保存原始输出,后者在同一套调用骨架上加一个工具再跑一次,看工具名和参数落在哪个字段里。两次都只依赖你手头能看到的原始返回和日志,不需要先接入完整 Agent 逻辑。
先确认单轮推理能拿到非空、可读的文本输出,再拿一次带工具调用的请求看工具名和参数放在哪一层。两步都只依赖你本地能看到的原始返回和日志,不依赖任何效果指标。若单轮无输出或工具段落无法稳定定位,先不要继续接入 Agent 解析,把提示模板和工具描述改到能复现再谈下一步。
用最小输入跑一次单轮推理并保存原始输出
目的不是测模型能力,是拿到一份最干净的基线输出,后面出问题时可以对照。请求里只放一条 user 消息,不挂工具,也不加系统提示词。用通用 HTTP 接入骨架就可以,字段名按你环境实际替换。
import requests
BASE_URL = 'https://your-endpoint'
API_KEY = '...'
payload = {
'model': 'iquest-q1',
'messages': [{'role': 'user', 'content': '只回复:pong'}],
'stream': False,
}
r = requests.post(BASE_URL + '/v1/chat/completions',
headers={'Authorization': 'Bearer ' + API_KEY},
json=payload)
open('run_single_raw.json', 'wb').write(r.content)
关键在落盘方式:用 wb 写 r.content,保留原始字节。不要写成 r.json() 再 dump,也不要在终端里先用 jq 美化后才存,那样会丢掉原始格式和可能的流式分片。如果接口默认返回 SSE 流,先把整段原始字节写进文件,再另存一份可读副本。验证时打开 run_single_raw.json,确认非空且能看到 choices 或你接入层的等价字段。风险边界:stream 默认值、请求路径、鉴权头名称可能不同,需要结合环境确认。
在输出里定位工具调用的标记段落
第二次请求保持调用方式不变,只在 payload 里加一个工具定义,然后把原始返回存成 run_tool_raw.json。工具调用通常落在 assistant message 的某个数组字段里,但不同实现在包裹层和字段名上有差异。先逐字摘出这一段,不要先改写它。
{
'choices': [
{'message': {
'role': 'assistant',
'content': None,
'tool_calls': [
{'id': 'call_abc',
'type': 'function',
'function': {'name': 'get_weather',
'arguments': 'city=杭州'}}
]
}}
]
}
标注字段名:choices[0].message.tool_calls 是工具调用数组;tool_calls[].id 是一次调用的标识;tool_calls[].function.name 是工具名;tool_calls[].function.arguments 是参数。会随工具不同的位置包括:工具名、参数内容、参数类型,以及有没有额外的包裹层。有的返回可能不出现 tool_calls,而是把调用写在 content 里用 XML 标签或自定义标记包起来。建议先用 jq '.choices[0].message.tool_calls' run_tool_raw.json 定位,找不到再全文搜索工具名。风险边界:不要假设所有工具都走同一个字段。
改一次工具参数再跑,比较两次返回结构
复制第一次的 payload,只改工具参数,比如第一次传 city=杭州,第二次传 city=杭州并加 units=metric,或者换成另一个工具。把原始返回另存为 run_tool_raw_2.json。然后对照两次返回的结构,而不是对照文本内容。
| 观察项 | 第一次返回 | 第二次返回 | 是否漂移 |
|---|---|---|---|
| tool_calls 路径 | choices[0].message.tool_calls | choices[0].message.tool_calls | 路径不变 |
| arguments 类型 | 字符串,如 city=杭州 | 字符串,如 city=杭州;units=metric | 类型未变 |
| 字段顺序 | id、type、function | type、id、function | 顺序变了,按字段名取值不受影响 |
| 额外包裹层 | 无 | 无 | 无 |
如果第二次出现 data 或 result 这类外层包裹,或者 arguments 从字符串变成对象,解析函数需要做归一化。如果只是字段顺序变,不影响按字段名取值。验证方式:对两次原始文件跑同一条定位命令,确认路径是否一致。风险边界:参数数量变化可能让部分实现从单工具调用变成多工具调用,需要单独看一次数组长度。
写解析函数把返回转成程序可用结构
定位到字段后,写一个解析函数把文本返回转成程序可用的字典,后续 Agent 逻辑不必直接处理原始文本。函数骨架如下,错误信息用固定前缀,方便在日志里搜索。
import json
def parse_tool_call(raw_text):
try:
payload = json.loads(raw_text)
except json.JSONDecodeError as e:
raise ValueError('RAW_NOT_JSON: %s at pos %s' % (e.msg, e.pos)) from e
try:
msg = payload['choices'][0]['message']
except (KeyError, IndexError, TypeError) as e:
raise ValueError('NO_MESSAGE_PATH: top_keys=%s' % list(payload.keys())) from e
calls = msg.get('tool_calls')
if not calls:
return {'kind': 'text', 'text': msg.get('content')}
out = []
for c in calls:
fn = c.get('function', {})
args_raw = fn.get('arguments')
if isinstance(args_raw, str):
try:
args = json.loads(args_raw)
except json.JSONDecodeError as e:
raise ValueError('BAD_ARGUMENTS: name=%s raw=%r' % (fn.get('name'), args_raw)) from e
else:
args = args_raw
out.append({'id': c.get('id'), 'name': fn.get('name'), 'args': args})
return {'kind': 'tool_calls', 'calls': out}
输入输出示例:把 run_single_raw.json 的原始内容喂进去,函数返回 {'kind': 'text', 'text': 'pong'}。把 run_tool_raw.json 的原始内容喂进去,函数返回 {'kind': 'tool_calls', 'calls': [{'id': 'call_abc', 'name': 'get_weather', 'args': {'city': '杭州'}}]}。解析失败时抛出固定前缀错误,如 RAW_NOT_JSON、NO_MESSAGE_PATH、BAD_ARGUMENTS。验证方式:两次原始文件分别过一遍解析函数,单轮走 text 分支,工具返回走 tool_calls 分支;如果工具返回报 RAW_NOT_JSON,先确认没有把流式 SSE 整段塞进来。
把解析失败的样本单独存档
解析失败不要只截一张图或只留美化后的片段,否则很难回头调提示模板或工具描述。建一个 failures/ 目录,每条失败存一个 JSON 文件,字段至少包括:
- raw_output:原始返回,保留原始字节或原样文本
- prompt:完整 messages,不要只留用户那一句话
- tool_schema:这次请求携带的工具 JSON Schema 或函数描述
- model:模型名或接入层标识
- error:解析函数抛出的错误文本,带上固定前缀
- timestamp:本地记录时间,便于按顺序排列
示例文件可以写成下面这样,字段名按你项目习惯调整,但 raw_output、prompt、tool_schema 三项建议不要省。
{
'raw_output': '...',
'prompt': [{'role': 'user', 'content': '查杭州天气'}],
'tool_schema': [{'name': 'get_weather',
'parameters': {'type': 'object',
'properties': {'city': {'type': 'string'}},
'required': ['city']}}],
'model': 'iquest-q1',
'error': 'BAD_ARGUMENTS: name=get_weather raw=...'
}
复现时用保存下来的请求体重跑。示意命令如下,rerun_tool.py 读 payload 文件发请求并把原始返回写到标准输出或文件,parse_check.py 调用上面的解析函数并打印结果。
python rerun_tool.py payload_get_weather.json > rerun_tool_raw.json
python parse_check.py rerun_tool_raw.json
重跑前确认 payload 里的 messages 和 tool_schema 没有被改写。如果错误前缀依旧出现,说明该失败样本可复现,可以拿它去调整提示模板或工具描述;如果重跑后解析通过,先保留原始失败样本和这次成功的原始返回,再决定是否回滚改动。