IQuest-Q1 想接进自己的 Agent 流程 / 工具调用格式该怎么对齐?

文章导读
把 IQuest-Q1 接进已有的 Agent 框架,真正卡住的地方通常不是模型本身,而是三层格式没对齐:提示模板里的角色标记跟模型侧训练用的模板不一致、自有的工具清单描述字段名对不上、模型吐出的工具调用片段不是严格 JSON。建议的处理顺序是先把模板对齐,再转工具描述,然后发一次最简请求把原始返回原样存盘,最后照着存下来的样本写解析层。反过来先写解析、再猜模型输出格式,遇到格式漂移时整条链路容易
📋 目录
  1. A 在仓库模板文件里确认角色标记与工具段落写法
  2. B 把自有 Agent 的工具清单转成模型可读的描述
  3. C 发一次最简工具调用请求并保存原始返回
  4. D 写解析层并处理非标准 JSON 的返回
  5. E 把单次调用接进多轮循环,检查第二轮上下文
A A

把 IQuest-Q1 接进已有的 Agent 框架,真正卡住的地方通常不是模型本身,而是三层格式没对齐:提示模板里的角色标记跟模型侧训练用的模板不一致、自有的工具清单描述字段名对不上、模型吐出的工具调用片段不是严格 JSON。建议的处理顺序是先把模板对齐,再转工具描述,然后发一次最简请求把原始返回原样存盘,最后照着存下来的样本写解析层。反过来先写解析、再猜模型输出格式,遇到格式漂移时整条链路容易直接断掉。

适用场景:自己已有 Agent 框架,只把 IQuest-Q1 当作推理与工具调用的一环。动作:逐项对照仓库模板文件里的角色标记与工具段落位置,把工具清单转成固定字段的描述,发一次最简请求保存原始返回,解析层按截取标记段、尝试解析、失败回填重试的顺序写。验证:看日志里第二轮请求的 messages 是否带上工具返回结果。边界:模型返回若不遵守标记约定,只能退化为纯文本解析,模板与解析层都要留兜底分支。

在仓库模板文件里确认角色标记与工具段落写法

这一步的目标不是改模型,而是让自己拼出来的提示结构和模型训练时使用的模板一致。先找到仓库里的模板文件(常见命名如 template、chat_template、prompt_config 之类),把它和你的 prompt 构造函数放在一起逐项对照。

grep -rn -e "tool_call" -e "assistant" -e "system" -e "eos" ./templates ./configs | head -50
对照项模板文件里要确认的内容自己框架需要改的字段
角色标记system / user / assistant 各自的起止写法拼 messages 时使用的 role 名与分隔符
分隔符每轮之间用什么符号切分,结尾是否带 EOS拼接函数里的 join 逻辑
工具段落位置工具定义是放在 system 内部,还是独占一段prompt 构造函数里工具段的插入点
工具结果回填结果用哪个角色名回填、字段名叫什么回填消息的 role 与 content 键名
调用输出标记模型输出工具调用时用什么包裹解析层用的正则或分割标记

对照完把不一致的字段列成一张改动清单再动手,比边跑边改容易定位问题。如果模板文件本身不可改,就要在自己的 prompt 构造函数里做适配,而不是在解析层兜。

把自有 Agent 的工具清单转成模型可读的描述

模型只能看到你写进请求里的文字,它不知道你代码里有哪些函数。工具描述要回答两件事:有哪些工具、每个工具的参数叫什么。下面是一个通用骨架,字段名和嵌套层级请按你所用框架的实际约定替换,不要直接照抄。

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_order_status",
        "description": "查询订单当前状态。只接受订单号,不接受其他条件。",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": {"type": "string", "description": "订单号,纯数字字符串"}
          },
          "required": ["order_id"]
        }
      }
    }
  ]
}

必填项通常是 name、description、parameters.type、parameters.properties、parameters.required。有的框架把这几个字段平铺,不套 function 一层,这时候要按框架要求改,而不是把两种写法混用。description 里写清楚边界(比如“不接受其他条件”“金额单位为元”),能减少模型编造参数的情况;参数层级不要超过三层,嵌套过深时模型容易漏填必填项。

IQuest-Q1 想接进自己的 Agent 流程 / 工具调用格式该怎么对齐?

发一次最简工具调用请求并保存原始返回

先只放一个工具、一句明确会触发该工具的问题,跑通一次即可。关键动作是把未经处理的返回原样写文件,不要在请求代码里顺手做字段提取,否则后面比对时看不到模型的真实输出。

import json, time, pathlib

resp = client.chat(
    model="iquest-q1",
    messages=messages,
    tools=TOOLS,
    tool_choice="auto",
)

stamp = time.strftime("%Y%m%d-%H%M%S")
pathlib.Path("raw").mkdir(exist_ok=True)
pathlib.Path(f"raw/{stamp}.json").write_text(
    json.dumps(resp, ensure_ascii=False, indent=2, default=str),
    encoding="utf-8",
)
print(resp)
</path>

把 raw 目录下的文件用编辑器打开,逐字看三件事:工具名是否原样出现、参数是否包在标记段里、标记段之外有没有多余解释文字。这三点决定了解析层要写成什么样。若客户端对象不能直接序列化,用 default=str 兜一下即可,目的只是留档。

写解析层并处理非标准 JSON 的返回

解析层要假设模型输出会漂移:标记段外有寒暄、JSON 带尾随逗号、参数被单引号包住、一次返回多个调用。按“截取标记段 → 尝试解析 → 失败则回填提示重试”的顺序写,任何一支失败都不要让主流程抛异常退出。

IQuest-Q1 想接进自己的 Agent 流程 / 工具调用格式该怎么对齐?
import json, re

CALL_RE = re.compile(r"<tool_call>(.*?)</tool_call>", re.S)

def parse_tool_calls(text):
    calls, errors = [], []
    for block in CALL_RE.findall(text or ""):
        block = block.strip()
        try:
            calls.append(json.loads(block))
            continue
        except json.JSONDecodeError as e:
            fixed = re.sub(r",\s*([}\]])`, r"\1", block)
            fixed = fixed.replace("'", '"')
        try:
            calls.append(json.loads(fixed))
        except json.JSONDecodeError as e2:
            errors.append({"block": block, "error": str(e2)})
    if not calls and not errors and text:
        errors.append({"block": text, "error": "no_tool_call_marker"})
    return calls, errors

回填重试时,把出错片段和错误原因一起放进下一条消息,并明确要求只输出 JSON,不要代码块围栏和解释文字:

def build_retry_message(errors):
    detail = "\n".join(
        f"- 片段:{e['block']}\n  错误:{e['error']}" for e in errors
    )
    return (
        "上一次返回的工具调用无法解析,请只输出一个合法的 JSON 对象,"
        "不要 Markdown 代码块围栏,不要解释文字。\n" + detail
    )

重试次数建议设成有限值,比如两次;仍失败就返回一个明确的错误结果给上层,而不是继续向模型追问。工具名不在注册表里、必填参数缺失这两类校验,也放在这一层做,错误信息同样按上面的格式回填。

把单次调用接进多轮循环,检查第二轮上下文

单次跑通后,用最小循环把结果回填回去,确认模型能接着往下走。下面的骨架里,MAX_TURNS 要设上限,避免模型反复要工具导致死循环。

messages = [{"role": "system", "content": SYSTEM},
            {"role": "user", "content": question}]

for turn in range(MAX_TURNS):
    resp = client.chat(model="iquest-q1", messages=messages, tools=TOOLS)
    text = extract_text(resp)
    calls, errors = parse_tool_calls(text)

    if errors:
        messages.append({"role": "user", "content": build_retry_message(errors)})
        continue
    if not calls:
        break

    messages.append({"role": "assistant", "content": text})
    for c in calls:
        result = dispatch(c)   # 本地执行,失败也要返回结构化错误
        messages.append({
            "role": "tool",
            "content": json.dumps(result, ensure_ascii=False),
        })
    log_messages(turn + 1, messages)

检查点从日志里看三项:第二轮请求的 messages 里,assistant 那条是否保留了模型原始的工具调用文本;工具结果条数是否等于本轮解析出的调用数;回填使用的 role 名是否和模板文件里确认的一致。任何一项对不上,问题通常在拼接层而不是模型侧。另外把 messages 的总长度打出来,必要时对历史轮次做截断,避免上下文被早期工具结果占满。