语义理解与工具调用:一句话怎么变成一次接口调用
会聊天不等于能干活。这一页把「去厨房把红色的盒子拿过来」拆成六格,
并如实摆出它只解析出一半的那次实测:模型给了 navigate_to + kitchen,把「红色的盒子」丢了。
01目标:一句话变成一次可执行的调用
'slots': {'location': 'kitchen'}}
(实测输出,未补字段)
上面这个 JSON 是板上真跑出来的。它有两个地方值得盯住: ① 结构是对的(intent + slots 两个键);② 内容是残缺的(「红色的盒子」没有出现在任何字段里)。
学完你该能做出这三件事
- 跑出一个结构化意图:给一句中文,拿到
intent与slots。 - 说出为什么必须自己做 JSON 提取与白名单校验,而不是直接
eval模型输出。 - 指出一次解析“看起来成功、其实漏了信息”的具体破绽。
🔑 一句话:这一页的产物不是“能解析”,是“解析完敢不敢直接执行”。
02问题:会聊天的模型,为什么不能让车动起来
导航指令、抓取指令、停止指令 —— 机器人的每个动作都对应一个确定的接口。 而模型只会输出一段文本,中间这段落差就是这一页要填的沟。
「去厨房」和 {"intent":"navigate_to"} 之间,缺的是什么?
模型可能给一个不存在的动作名、一个不存在的地点,谁来拦?
“去厨房 并且 拿红色的盒子” —— 这套结构撑得住吗?
🔑 一句话:模型的输出是“建议”,能不能执行由代码的边界决定。
03现象:真跑到的那一个 JSON
用本地 Qwen3.5-0.8B 做意图解析,提示词里写清“只输出 JSON、intent 取四个值之一”, 一句中文进去,一个 JSON 出来。下面的输出没做任何修饰。
JSON: {'intent': 'navigate_to', 'slots': {'location': 'kitchen'}}
intent = navigate_to | slots = {'location': 'kitchen'}
再看这套映射表(真源 car1_llm/config/skills.yaml)里的五个技能:
| 技能名 | 类型 | ROS 接口 |
|---|---|---|
navigate_to | action | /navigate_to_pose |
grasp_object | service | /visual_grasp/start |
go_home | service | /arm_controller/go_home |
stop | topic | /cmd_vel(线速度归零) |
transport | service | /navigation_grasp_transport/start |
怎么跑出来
用项目自带的 LLM 客户端,一句提示词就要一个 JSON:
终端一 · 意图解析
cd ~/ros2_ws/src/xflz_eai_car1/app/elderly_companion
python3 -c "
from common.config import load_config
from fall.config_schema import REQUIRED
from llm.server_manager import LLMServerManager
from llm.llm_client import LLMClient
cfg=load_config('config/fall_config.yaml',REQUIRED)
m=LLMServerManager(cfg['llm']); c=LLMClient(endpoint=m.ensure_running())
print(c.chat_json('只输出JSON:{\"intent\":\"navigate_to|grasp_object|stop|chat\",\"slots\":{}}。用户说:去厨房把红色的盒子拿过来'))
m.stop()"
🔍 看什么:三处。
- 看键名是否与提示词一致 —— 是
intent与slots,说明约束生效了。 - 看 slots 里有什么 —— 只有
location,没有颜色、没有物体名。 - 看有没有多余文字 —— 这次没有;但模型经常加一层 ```json 围栏,所以提取代码必须能剥。
🔑 一句话:现象里最值钱的不是“跑通了”,而是“跑通了却漏了一半”。
04原理:从句子到接口的六格
把“自由文本”压成“有限选项”,再按选项去查一张接口表。
模型只负责把话听懂成 intent + slots,剩下四格全是代码在守边界 —— 像填一张只有固定几个格子的表单,而不是让模型自己写命令。
| ② 的三个疑问 | 在 04 的哪几格回答 |
|---|---|
| 疑问 1 文本怎么变结构 | 步骤 1~4(提示词约束 + JSON 提取) |
| 疑问 2 为什么不能直接执行 | 步骤 5(白名单 + 查表映射) |
| 疑问 3 一句话两个动作 | 步骤 6 后的“结构上限”说明 |
1为什么不让模型直接说“执行 navigate_to”?
吃什么:用户中文。做什么:先想清“允许它说哪几个词”。吐什么:一个封闭集合。 动参数会怎样:集合越窄越安全,但越容易“听不懂”;越宽越灵活,越容易乱执行。
2提示词里到底要写清几件事?
吃什么:句子 + 任务说明。做什么:三件事必须写清:只输出 JSON、intent 的候选值、
slots 里各字段的含义与可选地点。吐什么:请求体。
动参数会怎样:少写候选值,模型会自己发明动作名(比如 go_kitchen),第 5 格直接拒绝。
3模型为什么爱加 ```json 围栏?
吃什么:请求体。做什么:生成。吐什么:字符串 —— 常见三种脏数据:
前面一句“好的,结果如下”;包一层围栏;JSON 后面再补一句解释。动参数会怎样:直接 json.loads 会当场炸。
4提取 JSON 为什么要“括号平衡扫描”?
吃什么:脏字符串。做什么:先剥围栏,再从第一个 { 开始数字符:遇到 { 加一、} 减一,
计数归零处截断;字符串里的括号要跳过。吐什么:一个 dict。
动参数会怎样:用正则 \{.*\} 贪心匹配,遇到嵌套对象会被截错。
5校验这一步在拦什么?
吃什么:解析出的 dict。做什么:三件事:intent 必须在白名单;
slots 里的地点必须能在 skills.yaml 的 mapping 里查到坐标;缺参就拒绝执行并追问。
吐什么:可执行参数,或一句“没听懂,请再说一遍”。
intent 字段。
一句话里两个动作,它必然只能选一个 —— 实测就是这样丢掉了「拿红色的盒子」。6调用那一层现在能跑吗?
吃什么:intent + 已映射参数。做什么:按类型走 action / service / topic。
吐什么:一次调用结果。
必须说清:本仓 car1_llm/car1_llm/skill_executor.py 里,
_call_service / _call_action / _publish_topic 三个方法只打了 TODO 日志,没有真正调用。
这一格是契约清楚、实现待补。别照它去指望机器人真的动。
每格答全的五列表
| 格 | 吃什么 | 做什么 | 用什么 | 吐出 | 动参数会怎样 |
|---|---|---|---|---|---|
| 1 | 中文句子 | 确定允许的表达范围 | 任务设计(非代码) | 封闭集合 | 集合过窄 → 听不懂 |
| 2 | 句子 + 约束 | 拼提示词 | 字符串模板 | 请求体 | 漏写候选值 → 发明动作名 |
| 3 | 请求体 | 本地推理 | RKLLM Qwen3.5-0.8B | 文本 | max_tokens 太小 → JSON 被截断 |
| 4 | 脏文本 | 剥围栏 + 括号平衡 | LLMClient.chat_json / _extract_json | dict | 用贪心正则 → 嵌套截错 |
| 5 | dict | 白名单 + 参数映射 | skills.yaml 的 skills / mapping | 可执行参数 | 不校验 → 执行不存在的接口 |
| 6 | 参数 | 调 ROS 接口 | skill_executor.py(方法存在,调用待补) | 调用结果 | 类型写错 → 调错通道 |
| 术语 | 一句话 | 什么时候失效 |
|---|---|---|
| intent | “用户想干什么”的枚举值 | 一句话多意图时只能选一个 |
| slots | 干这件事需要的参数 | 缺参就得追问,不能瞎填 |
| 白名单 | 只认表里有的动作名 | 表里没有的技能一律拒绝 |
| 参数映射 | 把“厨房”翻成坐标 | 没配的地点直接失败 |
🔑 一句话:模型负责“听懂”,代码负责“不许乱来” —— 五格在管后者。
05实践:三步把意图变成接口
- 看一眼技能表 约 10 分钟
打开
skills.yaml,确认五个技能名、类型与接口名 —— 这就是第 5 格的白名单来源。 - 跑一次意图解析 约 20 分钟
用
chat_json要一个 JSON,记录键名与 slots 内容,对照提示词看约束是否生效。 - 手工做一次映射 约 15 分钟
拿
location: kitchen去mapping里查坐标,写出这条指令最终会调哪个接口。
对账表:命令 → 函数 → 文件
| 格 | 命令 / 配置 | 函数或变量 | 文件 |
|---|---|---|---|
| 2 | 提示词字符串 | (调用方拼接) | 本页示例脚本 |
| 3 | llm.server.port / model_path | LLMServerManager.ensure_running | llm/server_manager.py |
| 4 | chat_json() | LLMClient.chat_json → _extract_json | llm/llm_client.py:23,147 |
| 5 | 技能表 | skills / parameters.mapping | car1_llm/config/skills.yaml:8,16 |
| 5 | (话题) | 标记格式 [SKILL:名|k:v] 与正则提取 | car1_llm/car1_llm/chat_node.py:154,159 |
| 6 | (未实现) | _call_service / _call_action / _publish_topic | car1_llm/car1_llm/skill_executor.py:90,117,123 |
踩坑速查
| 学员原话 | 原因 | 怎么办 |
|---|---|---|
| “解析报错 no JSON object” | 模型本次没吐 JSON(只说了句人话) | 在提示词里加“只输出 JSON,不要解释”,并保留重试一次 |
| “JSON 被截断了” | max_tokens 太小,右括号没生成出来 | 调到 200;括号不闭合时 _extract_json 会抛异常而不是猜 |
| “intent 是我没见过的名字” | 提示词没给候选值 | 补上四个候选值,并在第 5 格白名单拒绝未知项 |
| “地点对了但机器人不动” | 第 6 格实现仍是 TODO | 本仓现状即如此,先按接口表手工调用验证 |
| “一句话两个动作只执行了一个” | schema 只有一个 intent | 需要多意图就得改成 intents 数组或拆句 |
slots 里只有 location。
少的那半边(物体、颜色)不是解析丢了,而是第 5 格之前的 schema 里根本没这两项。🧪 Vibe Coding 复盘
| 想要 | 提示词 | 验收判据 |
|---|---|---|
| 模块职责 | 「这个文件里哪段负责从模型文本里取 JSON?它对脏数据做了哪几步处理?」 | 剥围栏 / 找第一个 { / 括号计数 / 字符串内跳过 |
| 控制流 | 「画出从句子到接口的流程图,标出所有会“拒绝执行”的分支」 | 至少三个拒绝点:解析失败 / 未知技能 / 缺参 |
| 时序 | 「LLM 服务、解析、映射三者的调用顺序与失败回退是什么?」 | 服务连不上要回退到规则匹配 |
| AI 常见说法 | 源码实际 |
|---|---|
| “用 json.loads 直接解析模型输出” | 先剥围栏 + 括号平衡扫描再 loads |
| “技能执行器会真的调服务” | 三个方法只有 TODO 日志 |
| “一个 JSON 能表达多个动作” | schema 只有一个 intent 字段 |
🔑 一句话:先确认“它说的词在表里吗”,再谈“它说得对不对”。
06交互:工具清单与输出长度
📌 掌握检查:把 max_tokens 拖到 60,JSON 会先坏在哪一处?先说出答案再拖。
三个观察点:
- max_tokens 太小 → JSON 右括号缺失 → 第 4 格抛异常,宁可失败也不猜。
- 工具条数变少 → 候选集变窄,模型更容易选对,但支持的说法也变少。
- 句子复杂度升到 3 → 仍只出一个 intent,缺口暴露在“第二个动作”上。
可信度说明:台子按实测那次输出(1 intent + 1 slot)与上面三条规则推演,不编造新的识别内容。
🔑 一句话:token 短了会截断,schema 窄了会丢意图 —— 两个限制方向相反。
07验证:三道题
Q1(原理)为什么不能把模型输出直接 eval 执行?
第 5 格的全部职责就是这条边界。
Q2(实践)_extract_json 先剥围栏再扫括号,是为了处理什么?
实测中围栏很常见,直接 loads 会失败。
Q3(边界)“去厨房把红色的盒子拿过来”只解析出 navigate_to,根因是什么?
解析代码没丢东西,是结构上限决定的。
✅ 目标对账
| ① 的目标 | 对应题号 |
|---|---|
| 能跑出结构化意图 | Q2(提取脏数据) |
| 能说清为什么不能直接执行 | Q1 |
| 能指出“成功但漏信息”的破绽 | Q3 |
🔑 一句话:三题分别考「边界 / 脏数据 / 结构上限」。
08总结:模型出意图,代码管边界
知识点关系表
| 概念 | 它解决了什么 | 和别的关系 |
|---|---|---|
| intent 白名单 | 限定能说什么动作 | 与 skills.yaml 一一对应 |
| slots | 承载参数 | 缺参要追问,不能默认 |
| JSON 提取 | 从脏文本里拿到对象 | 是校验的前一步 |
| 参数映射 | 把名字翻成坐标 | 没有映射就没有执行 |
| 接口类型 | 决定走 action/service/topic | 写错就调到错通道 |
两个反直觉
- 直觉是“JSON 合法就成功了”,实际是结构合法但语义残缺时,执行层会做出半件事(跑过去、不拿东西)。
- 直觉是“模型越强越不需要校验”,实际是模型越强说法越多,白名单越重要。
注意事项
- 数值陷阱:
max_tokens是输出上限,不是“越长越好”—— 太短会截断 JSON,太长会拖慢响应。 - 第 6 格在本仓是骨架(TODO),演示前先讲清,别让学员以为机器人真会动。
- 一句话多意图必须改 schema;靠调提示词解决不了。
- 地点不在
mapping里时,正确行为是拒绝执行并追问,不是默认一个坐标。
前置依赖
| 类别 | 具体 | 缺了会怎样 |
|---|---|---|
| 硬件 | 无特殊要求;执行层才需要底盘/机械臂 | 只影响“动作是否发生”,不影响解析 |
| 模型 | ~/models/rkllm/Qwen3.5-0.8B_w4a16_g128_rk3576.rkllm | 服务起不来,无法生成 JSON |
| 库 | rkllm 运行时 + pyyaml(读技能表) | 导入失败或技能表读不出 |
| 配置 | llm.enabled: true、skills.yaml 的五个技能与名单 | 无白名单则第 5 格无法校验 |
| 环境 | 能访问 127.0.0.1:8080 | 连不上要回退到规则匹配 |
这套东西用在哪儿
- 仓储机器人调度:仓管员说“去 A 区把蓝色托盘送来”,系统转成导航 + 抓取两条任务。
- 服务机器人点单:客人说“来杯热水”,转成一个取水动作而不是聊天回复。
- 产线语音调试:工程师说“回原点”“停”,直接映射到固定 service。
🔑 记忆点:模型可以幻觉,代码不能 —— 白名单、映射、拒绝执行,三件事一件都不能省。