IQuest-Q1 在本地跑起来之后,直接照 Agent 示例搭多轮工具调用,失败点往往不止一个:可能模型权重没加载成功,可能 chat 模板没把 system 与历史拼进去,可能工具描述写得含糊导致模型不选或选错,也可能是自己写的参数解析吞掉了报错。与其在一个大循环里靠猜,不如把链路拆成四层,每层设一个能反复复现的通过判据,哪层失败就回退到哪层重测。
IQuest-Q1 本地部署接 Agent,建议按「单轮推理 → 系统提示与历史 → 单工具单次调用 → 多工具选择 → 封装进循环」的顺序逐层验证。每层只用一个最小脚本和一份可核对的输出判据,通过后再往上加。一旦某层出现异常,就退回该层重测,不要在完整循环里同时调模板、工具和解析器。下面给出的检查点都能通过命令、日志和返回体核对,具体模板写法与字段名需要结合所用的本地推理框架确认。
用最小脚本跑通一次不带工具的单轮推理
这一层的唯一目的是确认权重能加载、分词器和生成链路能工作,跟 Agent 无关。脚本骨架(换成你本地实际使用的加载方式即可):
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
MODEL_DIR = '/path/to/iquest-q1'
tok = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR,
torch_dtype='auto',
device_map='auto',
trust_remote_code=True,
)
prompt = '用一句话说明什么是服务回滚。'
inputs = tok(prompt, return_tensors='pt').to(model.device)
out = model.generate(**inputs, max_new_tokens=128, do_sample=False)
print(tok.decode(out[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True))
启动命令就是 python single_turn.py。若走本地推理服务,也可以用 curl 打一次 chat 端点,但这次请求里不要带 tools 字段,否则又混入了工具层的问题。
判定成功的输出特征:进程退出码为 0,stdout 是一段完整、读得懂的中文,没有 traceback,不是空字符串,也没有立刻吐出结束符。如果出现乱码或停不下来的重复,先检查 special token 配置和 skip_special_tokens 的处理,而不是去改 Agent 提示词。
加上系统提示与两轮历史,验证上下文拼接
这一层确认的是:system 提示和多轮历史确实进入了同一次请求,并且顺序正确。把 messages 直接交给模板,然后把最终文本和 token 都打印出来逐段看。
messages = [
{'role': 'system', 'content': '你是运维助手,只回答部署与排障相关的问题。'},
{'role': 'user', 'content': '服务重启后端口不通,先看什么?'},
{'role': 'assistant', 'content': '先确认进程是否存活,再看端口监听情况。'},
{'role': 'user', 'content': '进程在,但端口没有监听。'},
]
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print('=== 最终请求体 ===')
print(text)
ids = tok.apply_chat_template(messages, tokenize=True, add_generation_prompt=True)
print('=== 前 60 个 token ===')
print(tok.convert_ids_to_tokens(ids)[:60])
核对三件事:system 是否排在最前;user 与 assistant 是否交替且历史顺序没被打乱;最后一条 user 之后是否带上了生成提示。常见的两种坑,一是框架只接受单条字符串,模板把 system 丢掉;二是历史被去重或截断。判据可以这样设:把最后一条 user 换成一个依赖前文结论的追问,模型能沿用上一轮 assistant 的说法作答。若回答像没看过历史,先在这一层改模板,不要急着挂工具。
只挂一个工具做单次调用
工具这一层最容易混进模板问题,所以先只挂一个。工具的 description 要写清「什么时候用」,参数描述要写清格式和示例,否则模型只能靠猜。
tools = [{
'type': 'function',
'function': {
'name': 'get_disk_usage',
'description': '查询指定主机上某个挂载点的磁盘使用率。只在用户明确问到磁盘空间时使用。',
'parameters': {
'type': 'object',
'properties': {
'host': {'type': 'string', 'description': '主机名或 IP,例如 db-01'},
'mount': {'type': 'string', 'description': '挂载点,例如 / 或 /data'}
},
'required': ['host', 'mount']
}
}
}]
messages = [{'role': 'user', 'content': '看下 db-01 上 /data 还剩多少空间。'}]
一次调用的返回样本,不同框架字段名会有差异,但信息位置是固定的:
{
'finish_reason': 'tool_calls',
'tool_calls': [
{
'id': 'call_1',
'name': 'get_disk_usage',
'arguments': {'host': 'db-01', 'mount': '/data'}
}
]
}
工具名出现在 name(有些框架是 function.name),参数出现在 arguments(有些框架是 function.arguments 字符串)。判定成功:模型没有直接编一段自然语言答案,而是返回了工具名,并且 host、mount 都命中提示里的内容。如果 arguments 是字符串形式的 JSON,先原样打印再 json.loads,解析失败时不要只留异常,把原始字符串一起记下来。
挂两个以上工具观察选择是否稳定
多工具场景下问题从「能不能调」变成「选得对不对」。建议再加一个语义相近的工具,例如磁盘使用率与磁盘 IO 延迟,两者都跟磁盘相关,最能暴露描述含糊。用同一句提示重复跑,把每次结果记下来。
轮次 | 提示 | 期望工具 | 实际选中工具 | 参数 | 是否缺参 | 原始返回
1 | 看下 db-01 磁盘 | get_disk_usage | | | |
2 | 看下 db-01 磁盘 | get_disk_usage | | | |
3 | ...
建议同一提示重复 5 次以上,do_sample=False 跑一组,采样式解码再跑一组,观察是否有轮次开始选错或漏参数。判据是:多次运行选中的工具是否一致、必填参数是否齐全、有没有把两个工具的参数串在一起。如果温度设为 0 时稳定而采样式解码就乱,说明工具之间的描述区分度不够,先改 description 里「只在什么情况下使用」的边界,再考虑解码参数。工具数量继续增加时,每个 name 的语义尽量独立。
封装函数接入 Agent 循环并记录失败点
前面四层都通过后,再把它们组装成一个循环。关键是每一步都留下能回退的断点,不要等最后报错再去猜。
def run_agent(messages, tools, max_steps=6):
for step in range(max_steps):
resp = call_model(messages, tools=tools) # 复用第 3 节验证过的单次调用
log_step(step, messages, tools, resp)
calls = parse_tool_calls(resp) # 解析失败 → 回退第 3 节
if not calls:
return resp.text
for call in calls:
args = check_args(call) # 缺参、错参 → 回退第 4 节
result = execute(call.name, args) # 执行异常 → 查工具函数本身
messages.append(tool_result(call.id, result))
return '超过最大步数,未收敛'
每轮日志建议至少打印这些字段:step;messages 条数与最后一条的 role 和 content;本轮是否携带 tools 以及工具名列表;模型原始返回文本;finish_reason;选中的工具名;参数原文与解析后的参数;工具执行结果的开头若干字符与异常栈。
失败时回退到哪一层,可以按下面这张对应关系判断:
- 报错堆栈、加载失败、输出乱码或停不下来 → 回退第 1 节,先确认推理链路本身。
- 模型无视历史、system 丢失、追问像第一次对话 → 回退第 2 节,检查模板拼接。
- 返回自然语言而不返回工具名、参数解析持续失败 → 回退第 3 节,先只挂一个工具。
- 工具选错、必填参数缺失或参数串到别的工具上 → 回退第 4 节,改工具描述并重复记录。
- 工具名和参数都对,但最终回答不对 → 问题在工具函数实现或结果拼回 messages 的方式,不必回退前面的层。
循环跑通之后再去考虑多轮工具串联、并发调用和重试策略。工具执行本身建议加超时与输入校验,避免一次失败让整条链路卡在半路。具体的字段名、模板写法和启动参数,需要结合本地实际使用的推理框架版本确认。