当前位置:首页 > 编程开发 > 正文

给本地模型接工具:llama.cpp + Function Calling 实战

约 5376 字 · 预计阅读 14 分钟
一句话总结:llama.cpp 的 server 已经支持 OpenAI 兼容的 Function Calling,只要模型自带工具模板、启动时加 --jinja,二十行 Python 就能让本地模型查数据库、读文件、调接口;难点不在接入,在「模型说错参数」时怎么兜住。

本地模型最大的短板不是智力,是「只能聊天」。真要干活,它得能调工具:查库存、读日志、发请求。

这篇用一个完整例子走通:llama.cpp 起服务 → 定义工具 → 模型返回 tool_calls → 执行 → 回填结果 → 模型总结。代码能直接跑。

前置条件

项目 要求
llama.cpp较新版本(支持 --jinja 与工具调用)
模型chat template 里带 tools 支持,如 Qwen2.5、MiniCPM5、Llama 3.1 系
显存2B 模型 Q4 约 2.5GB,7B Q4 约 5GB
Python3.10+,装 openai 和 pydantic

先确认你的 llama.cpp 支持 jinja 模板:

llama-server --help | grep -i jinja

有输出才能继续。没有就更新二进制,这个参数是工具调用的前提。

第一步:起服务

llama-server -m MiniCPM5-2B-Q4_K_M.GGUF \
  -c 8192 \
  -ngl 99 \
  --jinja \
  --host 127.0.0.1 \
  --port 8080

-c 8192 是上下文,工具定义会占掉几百 Token,太小会截断;-ngl 99 全量上卡;--jinja 让服务用模型自带的 chat template(而不是内置的简化模板),工具调用必须靠它。

第二步:定义工具

工具就是用 JSON schema 描述的函数。写两个够用的:

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "查询订单状态,返回当前物流节点",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "订单号,形如 SO20261026001"},
                },
                "required": ["order_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_docs",
            "description": "在本地文档库里检索关键词,返回最相关的三段",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "检索词"},
                    "top_k": {"type": "integer", "description": "返回条数,默认 3"},
                },
                "required": ["query"],
            },
        },
    },
]

description 写得越具体,模型选错工具的概率越低。这是最划算的优化,比换模型便宜。

第三步:跑完整的调用循环

import json
from openai import OpenAI
from pydantic import BaseModel, ValidationError

client = OpenAI(base_uRL="http://127.0.0.1:8080/v1", API_key="local")

class OrderArgs(BaseModel):
    order_id: str

def get_order_status(order_id: str) -> str:
    # 换成你的真实查询
    fake = {"SO20261026001": "已发货,杭州转运中心"}
    return fake.get(order_id, "未找到该订单")

def search_docs(query: str, top_k: int = 3) -> str:
    return f"[本地检索占位] 与「{query}」相关的前 {top_k} 段内容……"

def run_tool(name, args_raw):
    """统一入口:先校验参数,再执行"""
    if name == "get_order_status":
        try:
            args = OrderArgs(**json.loads(args_raw))
        except (ValidationError, json.JSONDecodeError) as e:
            return f"参数错误:{e}"
        return get_order_status(args.order_id)
    if name == "search_docs":
        a = json.loads(args_raw)
        return search_docs(a.get("query", ""), int(a.get("top_k", 3)))
    return f"未知工具:{name}"

def chat(user_text, max_rounds=4):
    messages = [
        {"role": "system", "content": "你是助手,需要数据时调用工具,不要编造。"},
        {"role": "user", "content": user_text},
    ]
    for _ in range(max_rounds):
        resp = client.chat.completions.create(
            model="minicpm5-2b",
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",
            temperature=0.1,
        )
        msg = resp.choices[0].message
        if not msg.tool_calls:
            return msg.content

        messages.append({
            "role": "assistant",
            "content": msg.content or "",
            "tool_calls": [tc.model_dump() for tc in msg.tool_calls],
        })
        for tc in msg.tool_calls:
            result = run_tool(tc.function.name, tc.function.arguments)
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result,
            })
    return "超过最大轮数,已中止"

print(chat("帮我查一下订单 SO20261026001 到哪了"))

三个细节决定成败:

tool_call_id 必须原样回填。 模型靠它把「结果」和「调用」配对,id 对不上它会重新调一遍,多花钱还容易死循环。

max_rounds 一定要有。 小模型偶尔会反复调同一个工具,没有上限就是无限循环。

参数先校验再执行。 OrderArgs(**json.loads(...)) 这一步能拦住绝大多数幻觉参数(订单号缺位、类型错)。

图 1:一轮完整循环:模型出 tool_calls → 校验参数 → 执行 → 回填 → 再生成

坑清单(按命中率)

1. 忘了 --jinja。 表现是模型把工具描述当普通文本复述一遍,永远不返回 tool_calls。这个坑占一半以上。

2. 模型本身不支持工具模板。 有些 GGUF 的 chat template 里没有 tools 分支,换模型比改代码快。选模型时先看模板文件里有没有 tools。

3. 只处理第一个 tool_call。 模型可能一次返回两个调用(并行),只回填一个会导致它下一轮再调另一个,甚至把两个结果混在一起。

4. 上下文被工具定义吃满。 两个工具约 400–600 token,-c 2048 的配置跑几轮就截断。至少给 8K。

5. 参数不校验直接执行。 让模型生成 SQL 或路径时尤其危险,小模型拼错一个引号就可能删库。所有副作用操作都要过一层校验 + 人工确认。

边界

2B 到 7B 的本地模型,工具调用准确率大概在 60%–75%(Bolt E4B 的 BFCL-v3 是 68.5,可作参考)。这意味着三分之一的场景会选错工具或给错参数。

所以定位要清楚:本地工具调用适合「读操作」和「低风险写操作」,比如查状态、检索文档、生成草稿。涉及转账、删数据、发消息给外部,必须加人工确认环节,别指望模型自己靠谱。

参考资料

目录导航
  • 前置条件
  • 第一步:起服务
  • 第二步:定义工具
  • 第三步:跑完整的调用循环
  • 坑清单(按命中率)
  • 边界
    • 参考资料
  • 相关文章:

  • MiniCPM5-2B 本地部署实战:4GB 显存跑起来,附三种跑法与坑清单2026-10-26 09:03:45
  • AI 编程的开源自建流:Cline、Aider、OpenCode 加上自己的 key,一个月要花多少钱2026-10-26 09:03:45
  • Function Calling 实战:schema 军规、三道闸与并发,200 次注入实测全绿之下还有 16.5% 错调2026-10-26 09:03:45
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。