大模型语义理解
EXP-04 的关键词匹配只能"对暗号";本实验让大模型正面回答「你想干什么」——意图识别、槽位提取、结构化 JSON 输出,再通过 Function Calling 把语义直接变成可执行的工具调用。
01问题引入:听懂字面 ≠ 听懂你
在 EXP-04 的工具模式里,机器人靠"抓→pick、放→place"这类关键词对暗号。这在几条固定指令内很好用,但只要用户换个说法就露馅:
| 你说的话 | 关键词方案的下场 | 缺的是什么 |
|---|---|---|
| "把胳膊抬起来歇会儿" | 没抓没放,匹配失败 | 近义意图归一(抬 ≈ 回工作位) |
| "先抓起来,然后放到右边" | 只命中一个词,顺序全丢 | 多意图 + 槽位(顺序、位置) |
| "学工专业"(含"工"字) | 误匹配成 work 动了机械臂 | 整句语义判断,而非子串匹配 |
| "附近有什么好吃的?" | 硬塞给 none,无回答能力 | 意图分类:查询 / 控制 / 闲聊 |
把一台只会关键词的机器人想象成一个只认门牌号的快递员——6 号门、8 号门他跑得飞快,但你说"送到那个有猫的家",他就原地宕机。语义理解要做的,就是把千变万化的说法翻译成统一、结构化、可执行的内部表示。
02原理通俗讲解:把话写成工单
把机器人后端想成一家餐厅的前厅写单流程。客人(用户)口头点菜,服务员(大模型)不直接冲进厨房,而是先在点菜单上写清楚:
| 生活比喻 | 技术概念 | 通俗解释 |
|---|---|---|
| 菜名——点了什么大类 | 意图 intent | "控制机械臂" / "查询信息" / "闲聊" 三选一,先定性。 |
| 口味备注——少辣、大份 | 槽位 slot | "放到右边"里的"右边"、"抓红色方块"里的"红色"。 |
| 工整的点菜单 | 结构化 JSON | {"intent":"control","action":"pick","pos":"right"}——机器可读,下一步谁看都懂。 |
| 菜单背面印好的厨具编号,报编号即开工 | Function Call(tools 声明) | 提前告诉模型"你手里有 calculator、move_arm 这几张牌",它直接报牌名+参数。 |
再把整条语音管线走一遍(本课程主线第 4 站就是今天的主角):
麦克风 16kHz 采集
"小飞小飞" 开启会话
ASR 吐出文字
意图 + 槽位 → JSON / tool_call
LLM 组织答复 → TTS 开口
Function Call 是本站的"临门一脚",本质是一个三步循环:
| 步 | 谁做 | 发生了什么 |
|---|---|---|
| ① 模型决策 | 大模型 | 响应里 content=None,换成 tool_calls=[{name:"calculator", arguments:"{\"expression\":\"1+1\"}"}] |
| ② 客户端执行 | 你的程序 | 查 FUNCTION_MAP 执行函数,拿到 {"result": 2} |
| ③ 结果回传 | 客户端 → 模型 | 追加 {"role":"tool", ...} 消息再次请求,模型基于真实结果说人话:"1+1 等于 2。" |
03关联知识点
/v1/chat/completions 请求体 {model, messages, stream, temperature…} 已成事实标准;RKLLM Server 原生实现,客户端零改造可迁移。
服务端逐行推 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 列表维护;system 角色必须放最前定义人设与输出格式,max_context_len=4096 超限截断。
tools(JSON Schema 报牌)→ tool_calls(模型出牌)→ role:tool(回传结果),缺任何一件循环不成立。
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"...]}
list_models(),连不上直接报 Cannot connect to server 退出。RKLLM 服务端单线程(threaded=False),并发请求会 503。06动画讲解:意图 → JSON → 行动流水线
观察一句话如何被"压"成机器行动:token 流逐段送入模型 → 分类出意图与槽位 → 拼成 JSON → 模型报出 tool_call → 客户端执行并回传 role:tool → 模型终答。4 秒循环,注意下方 Function Call 闭环的回环路径:
07互动讲解:意图分类与采样实验台
选一句话、换一组采样参数,看看同一个 0.8B 模型的"理解"会如何漂移——尤其是温度对结构化输出稳定性的影响,以及多意图指令的拆解方式。
intents:[…] 数组并带 order 序号槽位——真实系统里这就是 EXP-04 关键词方案做不到的"先…然后…"。08实操步骤
步骤 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"
09重点与易错点
| 现象 | 原因 | 解决 |
|---|---|---|
| 模型根本不调用工具,正常回文字 | tools 的 description 太模糊 / 问法太开放 | 动词开头一行中文("执行数学计算");问题更具体 |
| tool_calls JSON 解析失败 | 0.8B 偶发非标准 JSON | try/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 connect | RKLLM 单线程;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 开发。
理解了"说什么",还差"怎么说的"——下一站给对话加上情绪维度:文本情感与语音韵律的双模态感知。
1. Function Call 的"模型决策 → 客户端执行 → 结果回传"循环,相比纯 Prompt Engineering(让模型直接在文本里编答案),在鲁棒性、可解释性、可扩展性上各有什么优势?请用"计算器"与"让模型心算 123×456"对比说明。
2. 若 0.8B 模型频繁输出非标准 JSON,你会如何设计一个"JSON + DSL 双格式"容错解析器(提示:先 json.loads,失败再正则提取 name/arguments)?它对 Function Call 的落地有什么价值?