第 3 课
结构化输出、工具调用、语义搜索、路由模式与智能体
用 JSON Schema 约束本地模型抽取发票字段,写两轮工具调用让模型查天气,用 EmbeddingGemma 2 起向量服务做中文语义搜索,用不带模型的 llama serve 路由模式按需加载多个模型,最后按官方文档把本地模型接到 Codex CLI、ChatGPT 桌面版和 Pi。
课程目录第 3 / 3 课
学习位置仅保存在当前浏览器,有效期 180 天。
图文教材
本课依据:Llama 官方文档「API server」https://llama.app/docs/api 、「Running a server」https://llama.app/docs/serve 、「Using Llama App with Agents」https://llama.app/docs/agents ,以及官方模型页 https://llama.app/models (2026-10-27 核对,llama build b11852)。
这一课做完你会得到什么
让本地模型按 JSON Schema 输出可直接解析的数据,让它调用你写的 Python 函数,用本地向量模型做一个中文语义搜索,用路由模式在一个端口上同时提供多个模型,最后了解怎么把本地模型接到 Codex CLI 和 Pi 这类编程智能体上。
先按第 2 课的方式启动聊天模型(关闭思考,别名 qwen3.5-0.8b):
llama serve -hf ggml-org/Qwen3.5-0.8B-GGUF:Q8_0 \
-c 8192 -a qwen3.5-0.8b -rea off
第 1 步:结构化输出,抽取发票信息
response_format 里给出 JSON Schema,服务端会用语法约束生成过程,输出一定符合这个结构。新建 extract.py:
import json
from openai import OpenAI
client = OpenAI(base_url="http://localhost:9931/v1", api_key="x")
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"date": {"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},
"amount": {"type": "number"},
"currency": {"type": "string", "enum": ["CNY", "USD"]},
},
"required": ["name", "date", "amount", "currency"],
}
text = ("发票:杭州云栖科技有限公司,"
"开票日期 2026年10月20日,金额 1280.50 元。")
resp = client.chat.completions.create(
model="qwen3.5-0.8b",
messages=[{"role": "user", "content": "从下面文字抽取信息:\n" + text}],
response_format={
"type": "json_schema",
"json_schema": {"name": "invoice", "schema": schema},
},
temperature=0,
)
data = json.loads(resp.choices[0].message.content)
print(json.dumps(data, ensure_ascii=False, indent=2))
uv run --with openai python extract.py
对照结果(2026-10-27 实测):
{
"name": "杭州云栖科技有限公司",
"date": "2026-10-20",
"amount": 1280.5,
"currency": "CNY"
}
原文写的是「2026年10月20日」,输出被约束成了 2026-10-20;金额是数字,币种只能是枚举里的值。
实测踩到的两个坑:
- 日期正则要写
[0-9],不要写\d。用\d时服务端日志提示不支持,这个字段会退化成任意字符串。 - 官方 API 文档的示例把
schema直接放在response_format下面。在 b11852 上用这种写法实测时输出没有被约束(模型返回了中文键名);按 OpenAI 标准写法放进json_schema.schema才生效。上面的代码用的就是标准写法。
只要合法 JSON、不限结构时,用 response_format={"type": "json_object"}。
第 2 步:工具调用,让模型查天气
用 OpenAI 的 tools 参数声明函数,模型决定要不要调、传什么参数;你的代码执行函数,把结果以 role: tool 消息发回去,模型再组织最终回答。新建 tools_demo.py:
import json
from openai import OpenAI
client = OpenAI(base_url="http://localhost:9931/v1", api_key="x")
def get_weather(city: str) -> dict:
# 演示用的假数据,真实项目换成你的接口
return {"city": city, "temp_c": 18, "sky": "多云"}
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询某个城市当前天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "杭州现在天气怎么样?"}]
r1 = client.chat.completions.create(
model="qwen3.5-0.8b", messages=messages, tools=tools,
)
call = r1.choices[0].message.tool_calls[0]
args = json.loads(call.function.arguments)
print("模型要调用:", call.function.name, args)
result = get_weather(**args)
messages.append(r1.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
})
r2 = client.chat.completions.create(
model="qwen3.5-0.8b", messages=messages, tools=tools,
)
print("最终回答:", r2.choices[0].message.content)
uv run --with openai python tools_demo.py
对照结果(2026-10-27 实测,节选):
模型要调用: get_weather {'city': '杭州'}
最终回答: 杭州目前(根据查询结果)天气情况如下:
* **温度**:18°C
* **天气状况**:多云
官方说明:针对工具调用训练过的模型会用它自己的原生格式,其他模型走通用兜底格式。真实项目里要处理模型不调用工具的情况,即 tool_calls 为空时直接用 content。
第 3 步:本地向量模型做语义搜索
另开一个终端,在 9932 端口起一个向量(embedding)服务。--embedding 让服务提供 /v1/embeddings:
llama serve -hf ggml-org/embeddinggemma-2-GGUF \
--embedding --port 9932
这里用的是 llama.app 模型页里的 EmbeddingGemma 2(Apache 2.0),没写量化档位时这个仓库自动选了 Q8_0。官方文档示例用的是 unsloth/embeddinggemma-300m-GGUF,也可以。
新建 search.py,把三条规定和一个问题都转成向量,按余弦相似度排序:
import math
from openai import OpenAI
client = OpenAI(base_url="http://localhost:9932/v1", api_key="x")
docs = [
"报销需在费用发生后 30 天内提交发票。",
"年假按工龄计算,满一年 5 天,满十年 10 天。",
"办公室 Wi-Fi 密码每季度更换一次。",
]
query = "出差的票据最晚多久要交?"
def embed(texts):
r = client.embeddings.create(model="emb", input=texts)
return [d.embedding for d in r.data]
def cos(a, b):
dot = sum(x * y for x, y in zip(a, b))
return dot / (math.hypot(*a) * math.hypot(*b))
q, *ds = embed([query] + docs)
scores = [(cos(q, d), doc) for d, doc in zip(ds, docs)]
for score, doc in sorted(scores, reverse=True):
print(f"{score:.3f} {doc}")
uv run --with openai python search.py
对照结果(2026-10-27 实测):
0.802 报销需在费用发生后 30 天内提交发票。
0.709 年假按工龄计算,满一年 5 天,满十年 10 天。
0.695 办公室 Wi-Fi 密码每季度更换一次。
问题里没有「报销」「发票」这两个词,但意思最接近的那条排在第一。每条向量是 768 维。文档多了以后,把向量存进向量数据库,再把排名靠前的几条交给第 2 课的聊天模型回答,就是一个本地 RAG。官方还支持 --rerank 重排序模型(/v1/rerank 接口),本课未实测。
第 4 步:路由模式,一个端口多个模型
先停掉前面两个服务。不带模型启动 llama serve,就进入路由模式:服务列出缓存里所有模型,请求里写哪个模型名就按需加载哪个。
llama serve -cl
llama serve -rea off
对照结果(2026-10-27 实测,-cl 的输出):
number of models in cache: 2
1. ggml-org/embeddinggemma-2-GGUF:Q8_0
2. ggml-org/Qwen3.5-0.8B-GGUF:Q8_0
路由模式下请求必须写 model,名字用 /v1/models 列出的完整名:
curl -s http://localhost:9931/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "ggml-org/Qwen3.5-0.8B-GGUF:Q8_0",
"messages": [{"role": "user",
"content": "用 5 个字以内说你好"}],
"max_tokens": 20}'
对照结果(2026-10-27 实测,第一次请求含加载约 5 秒;只摘 model 和回答):
ggml-org/Qwen3.5-0.8B-GGUF:Q8_0 | 你好呀!
请求前后各查一次 /v1/models,模型状态从 unloaded 变成了 loaded:
ggml-org/Qwen3.5-0.8B-GGUF:Q8_0 loaded
ggml-org/embeddinggemma-2-GGUF:Q8_0 unloaded
启动时加的参数(这里是 -rea off)会带给每个按需加载的模型。除了缓存,还可以用 --models-dir 指定一个放 GGUF 文件的文件夹,或用 --models-preset 指定预设文件。
第 5 步:接到编程智能体
本节按官方 Agents 文档整理,未在云主机上实测:0.8B 模型不适合驱动编程智能体。官方建议上下文至少 32k token,并从 https://llama.app/models 里选标注 agentic coding 的模型(例如页面上写明 local agentic coding 的 GLM 4.7 Flash,标注需要 32 GB 以上内存)。
先用足够大的上下文起服务,例如:
llama serve -hf <模型仓库:量化> -c 32768
Codex CLI 和 ChatGPT 桌面版:安装 Codex CLI 后,在 ~/.codex/config.toml 里加入官方给出的配置,model 换成你实际服务的模型 id:
npm install -g @openai/codex
model = "ggml-org/Qwen3.5-35B-A3B-GGUF" # id of the model you serve
model_provider = "llamacpp"
[model_providers.llamacpp]
name = "llama.cpp"
base_url = "http://127.0.0.1:9931/v1"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false
然后在项目文件夹里运行 codex。ChatGPT 桌面版改完配置后退出重开即可。这里用的是 /v1/responses 接口(Responses API 格式),llama.cpp 在内部把它转换成聊天补全。
Pi:官方安装命令和接入步骤:
curl -fsSL https://pi.dev/install.sh | sh
cd /path/to/project
pi
在 Pi 里输入 /login llama.cpp,服务地址填 http://127.0.0.1:9931,再用 /model 选模型。
其他智能体:任何支持 OpenAI 兼容接口的工具,把 Base URL 设成 http://127.0.0.1:9931/v1 就能用。
小结
- 结构化输出用
response_format.json_schema.schema,正则写[0-9]; - 工具调用是两轮请求:模型给出
tool_calls,你执行后用role: tool回传; --embedding起向量服务,配合余弦相似度就能做中文语义搜索;- 不带模型的
llama serve是路由模式,请求里写哪个模型就按需加载哪个; - 接智能体要用大模型、32k 以上上下文,Base URL 都是
http://127.0.0.1:9931/v1。
