核心课程 / 自然语言交互应用 / EXP-05
EXP-05

语义理解与工具调用:一句话怎么变成一次接口调用

会聊天不等于能干活。这一页把「去厨房把红色的盒子拿过来」拆成六格, 并如实摆出它只解析出一半的那次实测:模型给了 navigate_to + kitchen,把「红色的盒子」丢了。

⭐ 8 学时意图识别JSON 约束工具白名单ROS 接口映射

01目标:一句话变成一次可执行的调用

给你
用户说:去厨房把红色的盒子拿过来
拿到
{'intent': 'navigate_to',
 'slots': {'location': 'kitchen'}}
(实测输出,未补字段)

上面这个 JSON 是板上真跑出来的。它有两个地方值得盯住: ① 结构是对的(intent + slots 两个键);② 内容是残缺的(「红色的盒子」没有出现在任何字段里)。

学完你该能做出这三件事

  • 跑出一个结构化意图:给一句中文,拿到 intent 与 slots。
  • 说出为什么必须自己做 JSON 提取与白名单校验,而不是直接 eval 模型输出。
  • 指出一次解析“看起来成功、其实漏了信息”的具体破绽。
⚠ 重难点 直觉是“模型输出 JSON 就算成功了”。实际是:结构合法 ≠ 语义完整。 这次实测里 JSON 完全合法,但两个动作被压成了一个 —— 机器人会跑去厨房,然后不知道要拿什么。

🔑 一句话:这一页的产物不是“能解析”,是“解析完敢不敢直接执行”。

02问题:会聊天的模型,为什么不能让车动起来

导航指令、抓取指令、停止指令 —— 机器人的每个动作都对应一个确定的接口。 而模型只会输出一段文本,中间这段落差就是这一页要填的沟。

疑问 1
文本怎么变成结构?

「去厨房」和 {"intent":"navigate_to"} 之间,缺的是什么?

疑问 2
为什么不能直接执行模型输出?

模型可能给一个不存在的动作名、一个不存在的地点,谁来拦?

疑问 3
一句话里有两个动作怎么办?

“去厨房 并且 拿红色的盒子” —— 这套结构撑得住吗?

先猜一下 三个疑问里,哪一个的答案在这个 JSON 结构本身里就已经注定解决不了? 记住你的选择,第 04 节步骤 5 后面有答案。

🔑 一句话:模型的输出是“建议”,能不能执行由代码的边界决定。

03现象:真跑到的那一个 JSON

🎬 现象速览 · 30 秒先看效果

用本地 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_toaction/navigate_to_pose
grasp_objectservice/visual_grasp/start
go_homeservice/arm_controller/go_home
stoptopic/cmd_vel(线速度归零)
transportservice/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()"
JSON: {'intent': 'navigate_to', 'slots': {'location': 'kitchen'}} ← 结构对,但「红色的盒子」不见了

🔍 看什么:三处。

  • 看键名是否与提示词一致 —— 是 intent 与 slots,说明约束生效了。
  • 看 slots 里有什么 —— 只有 location,没有颜色、没有物体名。
  • 看有没有多余文字 —— 这次没有;但模型经常加一层 ```json 围栏,所以提取代码必须能剥。

🔑 一句话:现象里最值钱的不是“跑通了”,而是“跑通了却漏了一半”。

04原理:从句子到接口的六格

CORE IDEA · 一句话原理

把“自由文本”压成“有限选项”,再按选项去查一张接口表。 模型只负责把话听懂成 intent + slots,剩下四格全是代码在守边界 —— 像填一张只有固定几个格子的表单,而不是让模型自己写命令。

② 的三个疑问在 04 的哪几格回答
疑问 1 文本怎么变结构步骤 1~4(提示词约束 + JSON 提取)
疑问 2 为什么不能直接执行步骤 5(白名单 + 查表映射)
疑问 3 一句话两个动作步骤 6 后的“结构上限”说明
一句话进 → 一次接口调用出 1 拿到一句自然语言 形态:中文句子,可能含两个动作 2 拼提示词:只许输出 JSON + 给可选值 形态:约束文本(intent 白名单、slots 结构、地点表) 3 模型生成一段文本(可能裹围栏) 形态:字符串(实测一次约 4 秒) 4 提取 JSON:剥围栏 + 平衡括号扫描 形态:dict(失败则抛解析异常,不猜) 5 校验 + 查表:intent 必须在白名单,地点必须有坐标 形态:(intent, 已映射参数) 或“拒绝执行” 6 调用 ROS 接口 + 回一句给用户 形态:一次 action/service/topic 调用 本仓现状:第 5 格有完整查表逻辑;第 6 格在 car1_llm 里仍是骨架(服务调用处写着 TODO)。 所以本页只把“说话”这一半做实,执行那半按接口表讲清契约。
图 4-1|六格链路。模型只占第 3 格,其余五格都是代码在收紧自由度。

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 里查到坐标;缺参就拒绝执行并追问。 吐什么:可执行参数,或一句“没听懂,请再说一遍”。

揭晓答案(疑问 3) 第 02 节里「结构本身解决不了」的是疑问 3:这份 schema 只有一个 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_jsondict用贪心正则 → 嵌套截错
5dict白名单 + 参数映射skills.yaml 的 skills / mapping可执行参数不校验 → 执行不存在的接口
6参数调 ROS 接口skill_executor.py(方法存在,调用待补)调用结果类型写错 → 调错通道
术语一句话什么时候失效
intent“用户想干什么”的枚举值一句话多意图时只能选一个
slots干这件事需要的参数缺参就得追问,不能瞎填
白名单只认表里有的动作名表里没有的技能一律拒绝
参数映射把“厨房”翻成坐标没配的地点直接失败

🔑 一句话:模型负责“听懂”,代码负责“不许乱来” —— 五格在管后者。

05实践:三步把意图变成接口

  1. 看一眼技能表 约 10 分钟 打开 skills.yaml,确认五个技能名、类型与接口名 —— 这就是第 5 格的白名单来源。
  2. 跑一次意图解析 约 20 分钟 用 chat_json 要一个 JSON,记录键名与 slots 内容,对照提示词看约束是否生效。
  3. 手工做一次映射 约 15 分钟 拿 location: kitchen 去 mapping 里查坐标,写出这条指令最终会调哪个接口。

对账表:命令 → 函数 → 文件

格命令 / 配置函数或变量文件
2提示词字符串(调用方拼接)本页示例脚本
3llm.server.port / model_pathLLMServerManager.ensure_runningllm/server_manager.py
4chat_json()LLMClient.chat_json → _extract_jsonllm/llm_client.py:23,147
5技能表skills / parameters.mappingcar1_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_topiccar1_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 数组或拆句
🔁 现象呼应 回到第 03 节那个 JSON:slots 里只有 location。 少的那半边(物体、颜色)不是解析丢了,而是第 5 格之前的 schema 里根本没这两项。

🧪 Vibe Coding 复盘

想要提示词验收判据
模块职责「这个文件里哪段负责从模型文本里取 JSON?它对脏数据做了哪几步处理?」剥围栏 / 找第一个 { / 括号计数 / 字符串内跳过
控制流「画出从句子到接口的流程图,标出所有会“拒绝执行”的分支」至少三个拒绝点:解析失败 / 未知技能 / 缺参
时序「LLM 服务、解析、映射三者的调用顺序与失败回退是什么?」服务连不上要回退到规则匹配
AI 常见说法源码实际
“用 json.loads 直接解析模型输出”先剥围栏 + 括号平衡扫描再 loads
“技能执行器会真的调服务”三个方法只有 TODO 日志
“一个 JSON 能表达多个动作”schema 只有一个 intent 字段

🔑 一句话:先确认“它说的词在表里吗”,再谈“它说得对不对”。

06交互:工具清单与输出长度

📌 掌握检查:把 max_tokens 拖到 60,JSON 会先坏在哪一处?先说出答案再拖。

互动 6-1 · 意图解析台
200
4
2
默认:4 个 intent 候选、句子含两个动作,实测输出只覆盖了第一个。

三个观察点:

  • 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。

🔑 记忆点:模型可以幻觉,代码不能 —— 白名单、映射、拒绝执行,三件事一件都不能省。