EXP-05 · 第 2 章 · 综合性 · 8 学时

大模型语义理解

EXP-04 的关键词匹配只能"对暗号";本实验让大模型正面回答「你想干什么」——意图识别、槽位提取、结构化 JSON 输出,再通过 Function Calling 把语义直接变成可执行的工具调用。

意图识别 · 槽位提取 Function Calling OpenAI 兼容 API RKLLM · Qwen3.5-0.8B 本地推理

01问题引入:听懂字面 ≠ 听懂你

在 EXP-04 的工具模式里,机器人靠"抓→pick、放→place"这类关键词对暗号。这在几条固定指令内很好用,但只要用户换个说法就露馅:

你说的话关键词方案的下场缺的是什么
"把胳膊抬起来歇会儿"没抓没放,匹配失败近义意图归一(抬 ≈ 回工作位)
"先抓起来,然后放到右边"只命中一个词,顺序全丢多意图 + 槽位(顺序、位置)
"学工专业"(含"工"字)误匹配成 work 动了机械臂整句语义判断,而非子串匹配
"附近有什么好吃的?"硬塞给 none,无回答能力意图分类:查询 / 控制 / 闲聊

把一台只会关键词的机器人想象成一个只认门牌号的快递员——6 号门、8 号门他跑得飞快,但你说"送到那个有猫的家",他就原地宕机。语义理解要做的,就是把千变万化的说法翻译成统一、结构化、可执行的内部表示。

💡 本节要点"语义理解"不是让模型自由发挥地聊天,而是三件事:分类意图(查询/控制/闲聊)、抽取槽位(对象、位置、数量)、约束输出(必须吐合法 JSON 或 tool_call)。可验证、可执行,才叫理解。

02原理通俗讲解:把话写成工单

把机器人后端想成一家餐厅的前厅写单流程。客人(用户)口头点菜,服务员(大模型)不直接冲进厨房,而是先在点菜单上写清楚:

生活比喻技术概念通俗解释
菜名——点了什么大类意图 intent"控制机械臂" / "查询信息" / "闲聊" 三选一,先定性。
口味备注——少辣、大份槽位 slot"放到右边"里的"右边"、"抓红色方块"里的"红色"。
工整的点菜单结构化 JSON{"intent":"control","action":"pick","pos":"right"}——机器可读,下一步谁看都懂。
菜单背面印好的厨具编号,报编号即开工Function Call(tools 声明)提前告诉模型"你手里有 calculator、move_arm 这几张牌",它直接报牌名+参数。

再把整条语音管线走一遍(本课程主线第 4 站就是今天的主角):

1
拾音
麦克风 16kHz 采集
→
2
唤醒
"小飞小飞" 开启会话
→
3
识别
ASR 吐出文字
→
4
理解 ★
意图 + 槽位 → JSON / tool_call
→
5
回复+合成
LLM 组织答复 → TTS 开口

Function Call 是本站的"临门一脚",本质是一个三步循环:

步谁做发生了什么
① 模型决策大模型响应里 content=None,换成 tool_calls=[{name:"calculator", arguments:"{\"expression\":\"1+1\"}"}]
② 客户端执行你的程序查 FUNCTION_MAP 执行函数,拿到 {"result": 2}
③ 结果回传客户端 → 模型追加 {"role":"tool", ...} 消息再次请求,模型基于真实结果说人话:"1+1 等于 2。"
🎯 核心直觉Prompt 是"嘱咐",Function Call 是"协议"。嘱咐可能被忘(模型胡说);协议把自由发挥限制在函数名 + JSON Schema 之内,这是 Agent、RAG、设备控制全部建立其上的地基。

03关联知识点

🔌
OpenAI 兼容 API

/v1/chat/completions 请求体 {model, messages, stream, temperature…} 已成事实标准;RKLLM Server 原生实现,客户端零改造可迁移。

🌊
流式 SSE

服务端逐行推 data: {json}\n\n,末条 data: [DONE];客户端 iter_lines() 取 delta.content 累积,首字延迟 < 500ms。

🎲
采样参数

temperature 0.8 / top_p 0.9 / max_tokens 4096 / repeat_penalty 1.1。分类任务要调低温度换确定性,创意任务再调高。

📜
多轮 messages

服务端无状态,历史由客户端 messages 列表维护;system 角色必须放最前定义人设与输出格式,max_context_len=4096 超限截断。

🃏
Function Call 三件套

tools(JSON Schema 报牌)→ tool_calls(模型出牌)→ role:tool(回传结果),缺任何一件循环不成立。

⚙️
RKLLM 本地推理

librkllmrt.so 经 ctypes 加载,W4A16 权重 4bit / 激活 16bit,0.8B 模型 NPU 约 30 token/s,全程离线不出板。

04实验目标

  • 理解 RKLLM 本地推理引擎与 W4A16 量化策略,会用 curl /v1/models 验证服务(原理理解)
  • 掌握 意图识别:把用户话术分类为 查询 / 控制 / 闲聊,能处理"多意图含糊指令"(分类实操)
  • 掌握 槽位提取(实体抽取):从句中抽出对象、位置、数量,拼装为结构化 JSON 输出(结构化实操)
  • 拆解 LLMClient / 流式 SSE 解析 / CLI 多轮对话三模块,完成 6 轮以上上下文记忆测试(模块拆解)
  • 理解 Function Call 原理,能在请求体声明 tools JSON Schema 并解析模型的 tool_call 指令(流程掌握)
  • 跑通"模型决策 → 客户端执行 → role:tool 回传"闭环:1+1=? → calculator → "等于 2"(集成实操)
  • 会用 system prompt 约束输出风格与 JSON 格式,理解必须 messages[0] 位置的原因(调优实操)
  • 能将该 OpenAI 兼容接口迁移到 Agent / MCP 等业务(迁移应用)

05实验环境与准备

类别配置
硬件具身智能实验箱 B2(RK3576 NPU 6TOPS)
系统Ubuntu 22.04 + ROS2 Humble,Python 3.10(ros2_venv)
核心库requests==2.31.0、flask==3.0.0、ctypes(标准库)
模型Qwen3.5-0.8B_w4a16_g128_rk3576.rkllm(约 0.8GB,max_context_len=4096)
运行时/usr/lib/librkllmrt.so(RKLLM 1.6.x,与 rkllm-toolkit 版本匹配)
源码ros2_ws/src/xflz_eai_car1/interaction/car1_llm/tools/(llm_server.py + llm_test.py)
# 0) 确认板载运行时存在
ls -l /usr/lib/librkllmrt.so        # 软链目标,llm_server 首启自动 symlink
# 1) 启动服务(终端 1)
cd ros2_ws/src/xflz_eai_car1/interaction/car1_llm/tools/
python3 llm_server.py               # =========init....=========== rkllm init success!
# 2) 客户端连通性(终端 2)
curl http://127.0.0.1:8080/v1/models # {"data":[{"id":"Qwen3.5-0.8B_w4a16_g128_rk3576"...]}
✅ 准备检查必须先起 llm_server、再起 llm_test——客户端会先调 list_models(),连不上直接报 Cannot connect to server 退出。RKLLM 服务端单线程(threaded=False),并发请求会 503。

06动画讲解:意图 → JSON → 行动流水线

观察一句话如何被"压"成机器行动:token 流逐段送入模型 → 分类出意图与槽位 → 拼成 JSON → 模型报出 tool_call → 客户端执行并回传 role:tool → 模型终答。4 秒循环,注意下方 Function Call 闭环的回环路径:

"把机械臂放到右边再抓" Qwen3.5-0.8B @ RKLLM (NPU) system: 只输出 JSON,字段 intent/slots { "intent": "control", "slots": {"pos":"right"} 语义三问 意图 → control(分类) 槽位 → pos=right(抽取) tool_calls move_arm(pos="right") 客户端 FUNCTION_MAP 执行 → {"ok": true, "pwm": …} role: "tool" 回传 → 模型终答"已放到右边" max_iter=3 兜底:模型反复要工具时强制截停,防死循环
动画 5-1:语义理解与 Function Call 闭环(4 秒循环)——紫色 token 流入模型,绿框吐出结构化 JSON,橘/蓝/绿三站组成「tool_calls → 客户端执行 → role:tool 回传」循环,最后一条绿线回到模型生成终答。理解的产物不是文字,而是可被程序消费的结构。

07互动讲解:意图分类与采样实验台

选一句话、换一组采样参数,看看同一个 0.8B 模型的"理解"会如何漂移——尤其是温度对结构化输出稳定性的影响,以及多意图指令的拆解方式。

意图分类与采样实验台 · Intent Lab
$ system: 你是语义解析器,只输出 JSON:{"intent":…, "slots":{…}}\n$ 点上方按钮发起一次 /v1/chat/completions 请求…
🔍 观察点温度 1.2 的控制句会偶现多余字段甚至裸文本(0.8B 小模型 + 高温 = 结构漂移),这正是"分类任务要低温"的直观证据;多意图句则拆成 intents:[…] 数组并带 order 序号槽位——真实系统里这就是 EXP-04 关键词方案做不到的"先…然后…"。

08实操步骤

1
起服务:llm_server + /v1/models 验证
→
2
跑通多轮对话与采样调参
→
3
system prompt 约束意图/槽位 JSON 输出
→
4
声明 tools,跑通 Function Call 闭环

步骤 1-2:服务与多轮对话(llm_test.py 默认采样 temperature=0.8 / top_p=0.9 / max_tokens=4096 / repeat_penalty=1.1):

python3 llm_test.py
# You > 我叫小明   → AI > 你好,小明!很高兴认识你。
# You > 我刚才说我叫什么? → AI > 你刚才说你叫小明。(messages 历史生效)
# You > clear 一键清空历史    You > exit 退出
# 采样对比:事实问题低温,创意任务高温
python3 llm_test.py --prompt "1+1=?" --temperature 0.1 --no-stream
python3 llm_test.py --prompt "写一首关于机器人的诗" --temperature 1.5

步骤 3:意图/槽位 JSON 输出——关键是 system prompt 放 messages[0] 并用白名单约束:

messages = [
  {"role": "system", "content":
   "你是语义解析器。只输出JSON:{\\"intent\\":查询|控制|闲聊, \\"slots\\":{...}},禁止解释。"},
  {"role": "user", "content": "把机械臂放到右边并抓取方块"},
]
# 期望: {"intent":"控制","slots":{"动作":["放置","抓取"],"位置":"右边"}}
resp = client.chat_once(messages, temperature=0.1, max_tokens=200)

步骤 4:Function Call 闭环——声明 TOOLS → 解析 tool_calls → 执行 → role:tool 回传(max_iter=3 兜底):

python3 llm_test.py --mode tools --prompt "现在几点了?"
# 可用工具: get_current_time, get_system_info, calculator, get_battery_status
# model → tool_calls: get_current_time() → FUNCTION_MAP 执行
# → {"role":"tool","name":...,"content":"{...}"} → 二次请求 → "现在是 14:30"
# 数学题同理: "1+1等于几?" → calculator(expression="1+1") → {"result": 2}
python3 llm_test.py --mode tools --prompt "计算 123 * 456"
⏱ 预期指标首字延迟 < 500ms(流式);端到端 < 3s;/v1/models 秒回;工具闭环通常 2 轮请求内出终答。

09重点与易错点

⚠️ 高频故障本实验的坑集中在协议细节:role 顺序、字段缺失、JSON 容错。0.8B 小模型"理解力有限",工程上要用约束与容错兜住它,而不是指望它每次都很乖。
现象原因解决
模型根本不调用工具,正常回文字tools 的 description 太模糊 / 问法太开放动词开头一行中文("执行数学计算");问题更具体
tool_calls JSON 解析失败0.8B 偶发非标准 JSONtry/except 包 json.loads,失败降级正则提取函数名(JSON+DSL 双格式)
role:tool 消息被服务端拒收缺 name 字段确保 {"role":"tool", "name":fn_name, "content":…}
工具执行崩了带崩整个流程FUNCTION_MAP 无异常保护lambda 内 try/except,回传 {"error": str(e)} 让模型自己圆场
tool_call 循环停不下来模型反复索要工具max_iter=3 强制截断,返回 "[max iterations reached]"
system prompt 不生效放在了 messages 末尾必须 messages[0];clear 历史后要重新 insert
多轮越聊越慢/截断历史逼近 max_context_len=4096只保留最近 N 轮;必要时摘要压缩
并发请求 503 / Cannot connectRKLLM 单线程;server 未启动请求排队;先起 llm_server.py 并等待 init success
推理速度忽快忽慢NPU/CPU/DDR 未锁频python3 llm_server.py --fix-freq(需 root,会真实调 sudo)
  • 每次实验前先 curl /v1/models 五秒排除连通性问题,再怀疑 prompt。
  • 调参遵循"一变量一次":先锁 temperature=0.1 验证格式,再谈其他。
  • 验证 JSON 时打印原始响应原文——别只看解析成功后的漂亮结果。
  • 新增工具只改两处:TOOLS(Schema)与 FUNCTION_MAP(实现),保持一一名映射。

10总结与思考

🏁
完成成果

llm_server + llm_test 多轮对话;意图/槽位结构化 JSON 输出;Function Call 闭环 calculator/get_current_time 实测通过;system prompt 风格约束生效。

🧠
原理收获

关键词匹配与语义理解的本质差异;tools / tool_calls / role:tool 三件套如何组成"决策—执行—回传"循环;W4A16 量化与流式 SSE 的工程取舍。

🛠
技能收获

OpenAI 兼容 API 调试、采样参数调优、messages 上下文管理、非标准 JSON 容错解析——这套本事可直接迁移到 Agent 与 MCP 开发。

➡
去向 EXP-06

理解了"说什么",还差"怎么说的"——下一站给对话加上情绪维度:文本情感与语音韵律的双模态感知。

💭 思考题

1. Function Call 的"模型决策 → 客户端执行 → 结果回传"循环,相比纯 Prompt Engineering(让模型直接在文本里编答案),在鲁棒性、可解释性、可扩展性上各有什么优势?请用"计算器"与"让模型心算 123×456"对比说明。

2. 若 0.8B 模型频繁输出非标准 JSON,你会如何设计一个"JSON + DSL 双格式"容错解析器(提示:先 json.loads,失败再正则提取 name/arguments)?它对 Function Call 的落地有什么价值?