端侧大模型意图识别与本地 Agent 编排【鸿蒙心迹】

摘要
本文记录在端侧(API 26 设备)用轻量小模型搭建 Agent 编排的完整实践。核心结论是:能跑模型不等于能做事,端侧 Agent 的落地底盘是「意图识别 + 工具闭环 + 受限编排」三件套。文章先给出可复用的 agentSafe 编排骨架(工具注册表、白名单校验、上下文裁剪、异常兜底),再用真机实测的 4 个踩坑案例(意图误判、工具参数缺失、上下文裁剪丢信息、步数上限触发)按「现象 → 原因 → 修复方式」逐一拆解,最后给出实测数据对比与可直接抄的实战清单。优化后意图识别准确率从 72% 提升到 91%,多步任务完整完成率从 63% 提升到 88%。
目录
一、背景与痛点
上一篇讲了"端侧 NPU 轻量大模型推理接入",能把一个小模型跑起来。但能跑模型不等于能做事——用户说"帮我总结这篇、再发给老张",需要模型先识别意图、再拆步骤、最后调本地能力完成。这就是端侧 Agent 编排。
我第一版把模型输出直接当结果,结果模型说"已帮你发送",其实啥也没干——它根本没调用任何能力。本质是只做了生成,没做"意图→工具调用"的闭环。本文讲清端侧意图识别、工具注册与本地编排循环。
「只生成」和「真编排」的差别,用一张图就能看清:
二、环境与能力规格
| 维度 | 规格 |
|---|---|
| 推理底座 | @kit.CoreAIKit / MindSpore Lite 端侧推理(API 26) |
| 意图解析 | 小模型输出结构化 intent + slots |
| 工具注册 | 本地能力(分享/收藏/搜索)注册为 tool |
| 编排循环 | 识别意图→调用 tool→回灌结果→再决策 |
| 适配建议 | 工具调用上限设 5 步,防死循环 |
从能力分层看,端侧 Agent 由下往上共四层,越往上越贴近用户,越往下越贴近硬件:
三、核心代码(可复用骨架)
// 1) 工具注册表
const tools = {
summarize: (text) => localSummarize(text),
share: (target) => localShare(target),
};
// 2) 编排循环:意图 -> 调工具 -> 回灌
async function agent(userSay: string) {
for (let i = 0; i < 5; i++) {
const plan = await llm.plan(userSay, ctx); // 输出 intent+slots
if (plan.intent === 'done') break;
const res = await tools[plan.intent](plan.slots);
ctx = ctx + `\n结果:${res}`; // 回灌模型
}
}
上面这段骨架刻意写得很短,是为了让结构一眼看清。真正落地时,还需要补上三块"工程化"的细节:工具白名单校验、上下文裁剪、异常兜底。下面给出一个更完整的版本,可以直接替换掉骨架里的 agent 函数。
// 3) 工具白名单:只有注册过的意图才允许执行
const ALLOWED = new Set(Object.keys(tools));
// 4) 上下文裁剪:只保留最近 3 步 + 关键结果,防止越聊越慢
function trimCtx(history: string[], keep = 3): string {
return history.slice(-keep).join('\n');
}
// 5) 完整编排:白名单 + 裁剪 + 异常兜底
async function agentSafe(userSay: string) {
const history: string[] = [`用户:${userSay}`];
for (let step = 0; step < 5; step++) {
const plan = await llm.plan(userSay, trimCtx(history));
// 终止条件
if (plan.intent === 'done') {
return plan.reply ?? '已完成';
}
// 白名单校验:未注册意图直接拒答,绝不硬调
if (!ALLOWED.has(plan.intent)) {
return '这个我暂做不到,换个说法试试?';
}
// 执行工具,异常不抛出,转为可回灌的结果
let res: string;
try {
res = await tools[plan.intent](plan.slots);
} catch (e) {
res = `执行失败:${(e as Error).message}`;
}
history.push(`动作:${plan.intent} 结果:${res}`);
}
return '步骤太多啦,我先停一下,你可以拆成更小的指令。';
}
整个编排循环的执行时序如下,可以对照代码理解每一步的输入输出:
从时序图能看出两个设计要点:第一,每一轮决策都必须带上上一轮的真实结果,否则模型无法判断下一步该做什么;第二,终止由模型显式给出 done,而不是靠步数耗尽来兜底,步数上限只是最后一道保险。
把 agentSafe 的每一轮循环抽象成状态机,可以更直观地看到「决策—校验—执行—回灌」的闭环:
四、运行效果与踩坑
下面这 4 个案例都来自真机(API 26 设备)实测,按「现象 → 原因 → 修复方式」整理,代码片段可直接对照 agentSafe 修改。
案例 1:意图误判——“总结"被识别成"搜索”
现象: 用户说"帮我总结一下这篇笔记",模型输出 intent=search, slots={keyword:"总结"},结果去本地搜索了"总结"两个字,返回空列表,用户一脸问号。真机 200 条指令抽样中,这类串味约占 28%。
原因: 提示词里 summarize 和 search 的意图描述都写了"处理文本内容",触发词高度重叠;且小模型对"总结/概括/提炼"这类近义表达泛化不足,容易退化成关键词匹配。
修复方式: 给每个意图补上触发词 + 反例 + 必填槽位三件套,并在解析后做一次槽位合法性校验,槽位不合法就降级重判。
const INTENT_SPEC = `
intent 只能是以下之一:
- summarize: 触发词「总结/概括/提炼/摘要」,slots 必须含 text(待总结正文)
反例:用户说"搜一下总结这个词" -> 属于 search,不是 summarize
- search: 触发词「搜索/查找/找一下」,slots 必须含 keyword
反例:用户说"总结这篇" -> 属于 summarize,不是 search
`;
// 槽位校验:必填槽位缺失或类型不符,视为误判,走一次重判
const REQUIRED: Record<string, string[]> = {
summarize: ['text'],
search: ['keyword'],
share: ['target'],
};
function validPlan(plan: any): boolean {
const need = REQUIRED[plan.intent];
if (!need) return false;
return need.every((k) => typeof plan.slots?.[k] === 'string' && plan.slots[k].length > 0);
}
改完后同一批抽样准确率从 72% 升到 91%,"总结/搜索"这一对混淆基本消失。
案例 2:工具参数缺失——share 拿不到目标人
现象: 用户说"把刚才的总结发出去",模型给出 intent=share, slots={},localShare(undefined) 直接抛异常,链路中断,用户只看到一句"出错了"。
原因: 多步指令里"发出去"的目标是上文隐含的(上一轮用户提过"发给老张"),但裁剪后的上下文只保留了最近 3 步的动作结果,把最初那句用户原话裁掉了,模型无从推断 target。
修复方式: 两层处理。第一层,把用户原始指令固定保留在上下文头部,不参与裁剪;第二层,槽位缺失时不让模型硬猜,改为向用户追问。
// 上下文结构:原始指令常驻 + 最近 N 步动作可裁剪
function buildCtx(userSay: string, history: string[], keep = 3): string {
const recent = history.slice(-keep).join('\n');
return `原始指令:${userSay}\n最近动作:\n${recent}`;
}
// 槽位缺失 -> 追问,而不是硬调工具
if (!validPlan(plan)) {
const missing = (REQUIRED[plan.intent] ?? []).filter((k) => !plan.slots?.[k]);
if (missing.length) {
return `还需要你补充一下:${missing.join('、')}`;
}
return '这个我暂做不到,换个说法试试?';
}
案例 3:上下文裁剪导致信息丢失——多步任务"忘了"第一步
现象: 三步指令"总结这篇 → 提取待办 → 发给老张",执行到第三步时模型回复"要发给谁?",明明第一步之后已经确认过目标。
原因: 裁剪策略是简单 slice(-3),按条数截断。而每轮会 push 一条动作记录,三步任务到第三轮时,最早那条"用户:发给老张"已经被挤出窗口。条数裁剪对"步数多、单条短"的场景不友好。
修复方式: 改为按 token 预算裁剪 + 关键信息钉住:用户原话和已确认的槽位(如 target)标记为 pinned,永不裁剪;其余动作按 token 从旧到新丢弃。
type Msg = { role: 'user' | 'action'; text: string; pinned?: boolean };
function trimByBudget(msgs: Msg[], maxTokens = 512): string {
const pinned = msgs.filter((m) => m.pinned);
const rest = msgs.filter((m) => !m.pinned);
let budget = maxTokens - pinned.reduce((s, m) => s + m.text.length, 0);
const kept: Msg[] = [];
// 从新到旧保留,直到预算耗尽
for (let i = rest.length - 1; i >= 0; i--) {
budget -= rest[i].text.length;
if (budget < 0) break;
kept.unshift(rest[i]);
}
return [...pinned, ...kept].map((m) => `${m.role}:${m.text}`).join('\n');
}
// 已确认槽位钉住
history.push({ role: 'action', text: `已确认 target=${plan.slots.target}`, pinned: true });
案例 4:步数上限触发——任务没做完就被打断
现象: 复杂指令"总结 → 提取待办 → 建日程 → 分享"跑到第 5 步时被强制中断,返回"步骤太多啦",但实际只差最后一步分享。
原因: 上限写死 5 步,且中断时没有把已完成的部分告诉用户,用户以为整个任务失败了,体验很差。另外模型偶尔会在两个意图间来回试探,白白消耗步数。
修复方式: 上限做成可配置,并在触顶时汇总已完成动作 + 给出续做入口;同时对"同一意图连续重复"做去重,避免无效消耗。
const MAX_STEPS = 6; // 按业务复杂度可调,默认 6
// 触顶时汇总已完成动作,而不是一句冷冰冰的提示
if (step === MAX_STEPS - 1) {
const done = history
.filter((h) => h.startsWith('动作:'))
.map((h) => h.split(' ')[0].replace('动作:', ''))
.join('、');
return `已完成:${done}。剩下的步骤要不要继续?回复"继续"我就接着做。`;
}
// 同一意图连续重复 -> 视为原地打转,提前终止
if (lastIntent === plan.intent && plan.intent !== 'done') {
return '我好像卡住了,换个更明确的说法试试?';
}
实测效果汇总
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 意图识别准确率 | 72% | 91% |
| 多步任务完整完成率 | 63% | 88% |
| 端到端耗时(两步指令) | 3.4s | 2.1s |
| 假执行(未调工具却称完成) | 频繁出现 | 0 例 |
| 工具异常导致链路中断 | 会中断 | 自动降级 |
五、小结与适用边界
端侧 Agent 不是把模型接上就完事,而是意图识别 + 工具闭环 + 受限编排三件套。适合离线助手、隐私敏感操作、本地自动化;不适合需要海量知识或强联网检索的复杂任务。
到底该不该用端侧 Agent,可以按下面这棵决策树快速判断:
六、实战清单(可直接抄)
- 所有"动作"必须走注册工具,模型只能决策不能自说自话,杜绝"假执行"。
- 意图用结构化
intent + slots输出,别信自由文本,准确率差一大截。 - 编排循环设步数上限(5 步)+ 显式
done终止,防模型陷入自我对话。 - 工具执行结果回灌模型上下文,让它能基于真实结果做下一步决策。
- 优先把"分享/收藏/搜索/总结"等本地能力工具化,形成可组合能力集。
- 真机测"多步指令",确认每步真实生效、链路能在离线下跑通。
端侧 Agent 的想象空间很大,但落地的底盘是"决策可信、动作可验"。把意图和工具钉死,小模型也能当靠谱的本地管家。
工程上还有一个关键:模型输出若不可信(如意图不在已注册工具里),要有"兜底拒答"而非硬调工具,否则可能误删或误发。我加了一层白名单校验,未命中意图直接回"这个我暂做不到",安全又诚实。此外,本地 Agent 的上下文窗口有限,多步循环里要定期裁剪历史,只留最近 3 步加关键结果,避免越聊越慢。把这些安全与效率的边界想清楚,端侧 Agent 才既聪明又可靠,敢于真正交给用户日常用。
最后用一张全景图收束全文,把「意图 → 工具 → 编排 → 兜底」四件事串成一条线:
更多推荐


所有评论(0)