旅行助手 Agent 实战之意图识别

众所周知,过去小长假经常出去玩,五一格鲁吉亚自驾游超详细记录国庆土耳其自驾游超详细记录曼谷、甲米、吉隆坡:一次没做攻略的旅行,恰逢最近前端失业转 Agent 中,索性做一个旅行助手的 Agent 作为找工作的准备。

产品介绍

产品主要用对话承载,可以聊天录入机酒、行程、小红书资料。

日程可以查看每日的安排,可以录入酒店、机票、地点、门票。机票、酒店也可以单独点击查看。

之前一些有用的小红书链接会发到微信群里,后边找的时候很麻烦,可以把小红书链接发给 Agent,会自动归档到资料里。

意图定义

最开始做的时候,并不知道「意图识别」是什么。最初的想法就是:用户说一句话,直接交给大模型,让它自己判断要不要调用工具、调用哪个工具,最后再生成回答。

这个想法很自然,因为大模型看起来什么都懂。用户说「第 3 天干嘛」,它知道是在问日程;用户发一个酒店订单截图,它也能看懂是酒店;用户说「这个地方安全吗」,它也能推断出和旅行有关。

所以最开始,想象中的系统大概是:

1
2
3
4
用户输入
-> 大模型理解
-> 大模型决定工具
-> 大模型回答

但真正往产品里做的时候,发现这条路并没有那么稳。后来又看了一些关于意图识别和 Agent 工程的资料,才意识到这里应该拆出一个更明确的路由层。

比如用户输入:

1
股票买哪个

如果全交给大模型,它可能真的开始分析股票。但我做的是旅行助手,它不应该回答投资建议,否则既越过了产品边界,也浪费 token。

再比如用户输入:

1
https://windliang.wang/ 加入到资料

这里有链接,也有「加入资料」的动作。如果不先判断范围,系统很容易直接抓取、总结、入库。但这个链接本身没有旅行语境,并不应该直接进入旅行资料库。

还有这种问题:

1
这个地方安全吗?

它确实和旅行有关,但也不能简单地当成普通问答。因为安全信息最好结合当前目的地、必要时搜索最新资料,并且给出来源。

Agent 不能只靠大模型自由发挥。它需要一个路由层,先判断这句话属于什么范围、对应哪个产品能力、是否需要追问、能不能继续执行。

也就是意图识别,不是给一句话贴标签,而是决定系统下一步该走哪条业务链路:查数据库、读附件、保存资料、搜索网页、写入行程、追问用户,还是直接拒绝。

意图梳理

项目是一个旅行助手,核心场景是帮用户管理一次旅行。

用户可以创建一个行程,比如:

1
2026 年国庆 · 西班牙

然后围绕这个行程做几类事。

1. 查询每日安排

用户可能会问:

1
2
第 3 天干嘛?
今天有什么安排?

这类问题对应到:

1
day_query

系统会读取当前行程里对应 Day 的安排,按时间整理出来。

2. 整理机票、酒店、门票订单

用户可以上传截图、PDF 或订单信息:

1
2
3
帮我整理这张机票
这个酒店订单记一下
门票二维码加到行程里

这几类分别对应:

1
2
3
flight
hotel
ticket

系统会提取航班时间、入住退房、门票日期、地址、订单号等结构化信息,并写入当前行程。

3. 导入行程表

比如用户上传 Excel、CSV,或者说:

1
2
把这个计划表导入
按这个表写进行程

对应的是:

1
itinerary

系统会解析日期、时间、地点、事项,然后写入每日安排。

4. 保存旅行资料

比如:

1
2
3
这篇西班牙攻略加入资料
小红书这个链接先存一下
这个景点官网留作参考

对应的是:

1
memory

系统会抓取链接内容,总结后放进当前行程的资料库。后续用户追问时,这些资料会成为上下文。

5. 分析攻略或截图

用户可能发一张小红书截图、攻略图片,然后问:

1
2
这张图里有什么推荐?
这个攻略适合我们吗?

对应的是:

1
note

它更像“资料阅读”和“攻略分析”,不一定马上写入行程。

6. 旅行通用问答

比如:

1
2
3
这个地方安全吗?
巴塞罗那有什么好吃的?
住的地方离机场远吗?

对应的是:

1
general

系统会结合当前行程、资料库、历史对话,必要时调用搜索或回答模型。

7. 需要追问

比如:

1
https://example.com 加入资料

这句话有「加入资料」的动作,但链接本身没有旅行语境。系统不知道它是不是旅行资料,所以我把它归到:

1
clarify

这时不应该直接入库,而应该先问:

1
这个链接和当前旅行有什么关系?

8. 超出范围

比如:

1
2
股票买哪个?
帮我写个劳动合同

对应的是:

1
out_of_scope

旅行助手不应该接这些任务。

所以,产品能力和意图的关系大致是:

1
2
3
4
5
6
7
8
9
10
day_query     -> 查询每日安排
flight -> 识别机票订单
hotel -> 识别酒店订单
ticket -> 识别门票/票券
itinerary -> 导入或写入行程
memory -> 保存旅行资料
note -> 分析攻略/截图
general -> 旅行问答
clarify -> 追问补充信息
out_of_scope -> 拒绝非旅行问题

意图识别不是为了给用户一句「分类结果」,而是为了决定系统下一步应该进入哪个产品能力。

为什么不直接让大模型判断所有事

如果系统足够简单,当然可以直接让大模型判断。

但旅行助手不是一个纯聊天机器人。它背后有数据库、有用户资料、有附件解析、有搜索、有写入动作。

这时,大模型判断错一次,后果不只是「回答差一点」。

它可能会:

1
2
3
4
5
把非旅行问题当成旅行问题回答
把普通链接错误保存进资料库
把「只是问问」理解成「写进行程」
把需要追问的问题直接执行
把多个意图压成一个意图

尤其是带写入动作的场景,我不敢让模型凭感觉决定。

比如:

1
这个加进去

这里的「这个」是什么?是上一条攻略?是一个景点?是一张门票?还是用户只是想保存资料?

如果系统直接让大模型调用写入工具,就很危险。

更接受的做法是:

1
2
3
4
高确定性的,用规则快速判断
模糊的,用便宜模型辅助分类
需要执行的,始终由业务代码控制
不确定的,追问用户

也就是说,大模型可以参与意图识别,但不应该独占意图识别。

那回到意图识别,可以把它看成一个从粗到细的路由过程:

1
2
3
4
先把用户的话整理清楚
再判断这是不是旅行助手该处理的范围
然后识别它对应哪个产品能力
最后才决定要不要调用模型、搜索、读附件或写数据库

对应到项目里,就是三层:

1
2
3
第一层:输入归一化
第二层:旅行范围判断
第三层:业务意图识别

这三层解决的问题不一样。

输入归一化解决的是:用户说得太口语、太省略,系统要先把话补完整。

旅行范围判断解决的是:这件事是不是旅行助手该接的,不能什么都往后丢。

业务意图识别解决的是:如果它属于旅行场景,那到底应该走日程查询、订单识别、资料保存、攻略分析,还是旅行问答。

这样拆完之后,大模型就不是整个系统的入口,而是其中一个兜底判断器。规则能确定的,直接走规则;规则不确定的,再让便宜模型帮忙复判;真正执行时,仍然由业务代码控制。

第一层:先把用户的话整理一下

真实用户不会按照系统字段说话。

他们会说:

1
2
3
4
机酒帮我整理一下
住哪来着
这个地方安全吗?
把刚才那个加进去

这里有缩写、口语、代词、省略和上下文引用。

所以现在先做输入归一化。

例如:

1
2
3
4
5
机酒 -> 机票 酒店 机酒
住哪 -> 酒店 住宿
怎么过去 -> 怎么去 交通
最划算 -> 推荐 价格 预算
这里 / 这个 / 当地 -> 补上当前行程名称和目的地

代码大概是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function normalizeInputForIntent(input: string, trip?: Trip) {
let normalized = input
.normalize("NFKC")
.replace(/\s+/g, " ")
.replace(/[??]+$/g, "")
.trim();

const replacements: Array<[RegExp, string]> = [
[/机酒/g, "机票 酒店 机酒"],
[/住哪|住哪里|住宿地/g, "酒店 住宿"],
[/怎么过去|如何过去/g, "怎么去 交通"],
[/最划算|省钱|性价比/g, "推荐 价格 预算"],
];

for (const [pattern, replacement] of replacements) {
normalized = normalized.replace(pattern, replacement);
}

const tripTokens = trip ? getTripReferenceTokens(trip).join(" ") : "";

if (tripTokens && /(这里|那里|当地|这个|它|那边)/.test(normalized)) {
normalized = `${normalized} ${tripTokens}`;
}

return normalized.trim();
}

原始输入:

1
这个地方安全吗?

如果当前行程是“2026 年国庆 · 西班牙”,归一化后,系统就能把“这个地方”放回当前行程语境里判断。

这一步的目的不是润色文本,而是把用户的自然表达变成系统更容易判断的表达。

第二层:判断是不是旅行范围

旅行范围有三种:

1
type TravelScope = "travel" | "non_travel" | "uncertain";

例如:

1
2
3
4
5
6
7
8
股票买哪个
=> non_travel / out_of_scope

https://windliang.wang/
=> non_travel / out_of_scope

https://windliang.wang/ 加入资料
=> uncertain / clarify

代码里会先做范围判断:

1
2
3
4
5
6
7
8
9
const rawQuestion = getActualQuestion(message);
const actualQuestion = normalizeInputForIntent(rawQuestion, trip);

const travelScope = resolveTravelScope({
file,
input: actualQuestion,
rawInput: rawQuestion,
trip,
});

如果是明确非旅行问题,就直接变成 out_of_scope

1
2
3
4
5
6
7
8
9
10
if (travelScope.scope === "non_travel") {
const intent: AssistantIntent = "out_of_scope";

return finishTurn({
ok: true,
title: "这个问题和旅行行程无关哦",
detail: "",
intent,
});
}

这一步其实是在回答一个更基础的问题:

这个请求是不是旅行助手应该处理的?

只有这个问题过了,才继续判断它是 memoryhotelticket 还是 general

第三层:判断具体业务意图

当系统确认请求属于旅行范围,或者至少不确定但可能相关时,才进入业务意图识别。

当前项目的意图有十种:

1
2
3
4
5
6
7
8
9
10
day_query
flight
hotel
ticket
itinerary
memory
note
general
clarify
out_of_scope

这些意图不是标签,而是后续链路。

例如:

1
2
3
4
5
6
7
8
9
10
day_query
=> 读取 Day 日程
memory
=> 抓取链接,总结并保存资料
clarify
=> 不执行,先追问
out_of_scope
=> 不处理,提示超出旅行范围
general
=> 进入旅行问答,必要时搜索或调用回答模型

规则识别会输出候选意图、分数和原因:

1
2
3
4
5
type IntentCandidate = {
intent: AssistantIntent;
reason: string;
score: number;
};

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (isDayScheduleQuestion(normalizedInput)) {
return {
candidates: [
{
intent: "day_query",
reason: "命中 Day/当天日程查询",
score: 100,
},
],
intent: "day_query",
normalizedInput,
originalInput: input,
};
}

再比如机票、酒店、门票:

1
2
3
4
5
6
7
8
9
10
11
if (/机票|航班|起飞|落地|登机|flight/.test(combined)) {
addCandidate("flight", 88, "命中机票/航班信号");
}

if (/酒店|入住|退房|住宿|hotel/.test(combined)) {
addCandidate("hotel", 88, "命中酒店/住宿信号");
}

if (hasTicketSignal && (file || hasSaveSignal)) {
addCandidate("ticket", 96, "命中票券信号,且有附件或保存动作");
}

最后按分数选出当前主意图:

1
2
3
4
5
6
7
const [winner] = [...candidates].sort((left, right) => {
if (right.score !== left.score) {
return right.score - left.score;
}

return candidates.indexOf(left) - candidates.indexOf(right);
});

链接的例子

旅行助手里有一个典型场景:用户发链接。

最开始的规则很简单:

1
只要输入包含 URL,就识别为 memory

也就是保存到资料库,但这样用户随便发一个个人网站,也会被当成旅行资料。

所以把规则更加细化:

1
2
3
4
旅行平台链接 -> 可以倾向 memory
普通链接 + 旅行语境 + 保存动作 -> memory
普通链接 + 保存动作 + 无旅行语境 -> clarify
普通裸链接 + 无旅行语境 -> out_of_scope

代码里会先提取安全 URL,并排除 localhost、内网地址这类不适合抓取的链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getSafeMemoryUrl(input: string) {
const rawUrl = extractFirstUrl(input);

if (!rawUrl) {
return undefined;
}

const url = new URL(rawUrl);

if (!["http:", "https:"].includes(url.protocol)) {
return undefined;
}

if (isPrivateMemoryHost(url.hostname)) {
return undefined;
}

return url;
}

然后判断它是不是旅行平台、有没有旅行文本、有没有保存动作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function resolveUrlTravelSignal(input: string) {
const url = getSafeMemoryUrl(input);

if (!url) {
return undefined;
}

const travelHost = hasTravelUrlHost(url);
const travelText = hasTravelUrlTextSignal(input, url);
const saveSignal = hasMemorySaveSignal(input);

return {
shouldSaveMemory: travelHost || (travelText && saveSignal),
travelHost,
travelText,
saveSignal,
url: url.toString(),
};
}

最后落到不同意图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (urlSignal?.shouldSaveMemory) {
return {
intent: "memory",
candidates: [
{
intent: "memory",
reason: "旅行链接或旅行资料保存信号",
score: 92,
},
],
};
}

if (urlSignal?.saveSignal && !urlSignal.travelHost && !urlSignal.travelText) {
addCandidate("clarify", 74, "只有保存动作,但缺少旅行语境");
}

比如:

1
2
3
4
5
6
7
8
9
10
11
小红书攻略链接 加入资料
=> memory

这篇西班牙攻略加入资料 https://example.com
=> memory

https://example.com 加入资料
=> clarify

https://example.com
=> out_of_scope

这里的关键是:动作不等于意图成立。

「加入资料」只是动作信号。系统还要判断这个资料是不是属于当前旅行场景。

如果不确定,就追问,而不是硬执行。

规则和模型怎么分工

不能完全靠规则,也不能完全靠模型。

规则适合高确定性的场景:

1
2
3
4
5
第 3 天干嘛 -> day_query
机票订单 -> flight
酒店订单 -> hotel
门票二维码 -> ticket
股票买哪个 -> out_of_scope

规则快、便宜、稳定。能确定的事情,不需要花一次模型调用。

但规则也有边界。

比如:

1
2
3
4
这个能不能带老人去?
这个链接有用,先放着
把刚才那个加进去
住的地方离机场远吗?

这些句子依赖上下文、代词和隐含动作。继续堆正则,会越来越脆。

所以引入了一个便宜模型:

1
glm-4.7-flash

它只在规则不确定时调用。

触发条件在代码里大概是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function shouldUseIntentClassifier({
actualQuestion,
intentResolution,
rawQuestion,
travelScope,
}: Args) {
if (travelScope.scope === "uncertain") {
return true;
}

const [topCandidate, secondCandidate] = sortedCandidates;

if (topCandidate.intent === "general" && topCandidate.score <= 70) {
return true;
}

if (topCandidate.intent === "clarify") {
return true;
}

if (topCandidate.score < 80) {
return true;
}

if (secondCandidate && topCandidate.score - secondCandidate.score <= 8) {
return true;
}

if (/(这个|那个|它|刚才|当地|这里|那里)/.test(rawQuestion)) {
return true;
}

return Boolean(resolveUrlTravelSignal(rawQuestion));
}

模型只做结构化分类:

1
2
3
4
5
6
7
8
9
{
"scope": "travel",
"intent": "general",
"confidence": 0.82,
"reason": "用户在询问当前目的地安全情况",
"slots": {
"needs_search": true
}
}

调用时会限制它的行为:

1
2
3
4
5
6
7
const callOptions = {
model: "glm-4.7-flash",
responseFormat: "json",
temperature: 0,
maxTokens: 300,
thinking: "disabled",
};

它不负责回答,不负责搜索,不负责写库。

也就是说,现在的分工是:

1
2
3
规则负责确定性
小模型负责模糊判断
业务代码负责执行

为什么模型不能直接执行

因为「理解」和「执行」是两件事。

模型可以判断:

1
用户可能想把这个攻略加到行程里

但真正要不要写入数据库,应该由代码判断:

1
2
3
4
5
有没有明确写入动作?
有没有目标 Day?
有没有结构化地点和时间?
用户是否确认?
工具是否返回 success?

当前项目里有一个原则:

只有工具返回成功,系统才能说“已写入”。

代码里会检查写入工具是否真的成功:

1
2
3
if (isSaveItineraryToolCall(toolCall) && isSuccessfulToolWrite(result)) {
hasSuccessfulItineraryWrite = true;
}

如果模型没有成功调用写入工具,却在回答里声称已经写入,系统会拦截:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (
answer &&
!hasSuccessfulItineraryWrite &&
isExplicitItineraryWriteRequest(question) &&
claimsItineraryWrite(answer)
) {
return {
ok: false,
title: "还没有写入行程",
detail: "刚才模型没有完成写入工具调用,所以我没有把它当成已写入。",
intent: "general",
};
}

这就是为什么不想让模型直接接管执行。

Agent 的可靠性,不来自模型一句「我认为」,而来自流程里的硬约束。

代码不是不相信模型的文字,而是先确认模型调用了 save_itinerary_items,再确认工具返回 success: true。只有这两个条件同时满足,系统才允许说“已写入”。

调试意图

为了调试给项目做了两件事。

第一,页面上有 Debug 面板,会展示:

1
2
3
4
5
6
7
输入归一化
旅行范围判断
规则意图识别
发送给 GLM · 意图分类
GLM 返回 · 意图分类
意图分类模型已采纳
最终意图

代码里每个关键节点都会写 Debug:

1
2
3
4
5
6
7
8
9
10
11
pushDebugEntry(debugEntries, "输入归一化", {
actualQuestion,
rawQuestion,
});

pushDebugEntry(debugEntries, "旅行范围判断", travelScope);

pushDebugEntry(debugEntries, "规则意图识别", {
candidates: intentResolution.candidates,
intent: intentResolution.intent,
});

第二,每一轮 Debug 都会落到本地文件:

1
.agent-debug/

简化后的代码是:

1
2
3
4
5
6
7
8
9
10
11
async function writeAgentDebugLog({ entries, response, tripId }: Args) {
const debugRoot = path.join(process.cwd(), ".agent-debug");
const filePath = path.join(debugRoot, `${Date.now()}.json`);

await mkdir(debugRoot, { recursive: true });
await writeFile(
filePath,
JSON.stringify({ entries, response, tripId }, null, 2),
"utf8",
);
}

这样可以回看每一次判断为什么出错。

比如看到一条:

1
https://windliang.wang/ 加入到资料

如果结果是:

1
memory

说明规则过宽。

如果结果是下边的就更合理。

1
2
3
scope: uncertain
intent: clarify
reason: 只有保存动作,但缺少旅行语境

没有 Debug,意图识别只能靠猜。有了 Debug,就能像调业务系统一样调 Agent。

当前架构和优化

当前项目的整体流程是:

image-20260826185210616

当前这套已经比「一句 Prompt 识别意图」稳很多,但产品毕竟还没经历大量真实用户输入,用户多了肯定会有各种意料之外的问题,后续还有很多方向可以优化。

尤其是现在规则层里还有不少正则。正则不是不能用,它适合处理高确定性的信号,比如:

1
2
3
4
第 3 天干嘛 -> day_query
机票 / 航班 / 起飞 / 落地 -> flight
酒店 / 入住 / 退房 -> hotel
股票 / 基金 / 炒股 -> out_of_scope

这些规则快、便宜、可控。

但问题也很明显:正则很容易越写越厚,最后变成一张很难维护的网。

比如:

1
2
3
4
5
6
7
住哪来着
住宿在哪
我们那天睡哪
酒店离机场远吗
这个地方适合老人吗
这篇先放着
这个加进去

它们表达的是相近意思,但关键词不一定稳定。继续补正则,短期能解决一个 case,长期会出现几个问题:

1
2
3
4
5
覆盖不全:用户换个说法就识别不到
误伤:关键词出现在无关语境里也会命中
边界重叠:同一句话同时像 memory、general、itinerary
难以解释:规则越多,最后很难知道为什么命中
难以回归:修一个 case,可能弄坏另一个 case

后续可以有下边的优化点:

1. 把单意图升级成多意图

现在系统本质上还是单意图。

比如:

1
机酒帮我整理一下

它其实同时包含:

1
2
flight
hotel

再比如:

1
这篇攻略不错,帮我加到第 3 天

它可能同时包含:

1
2
memory
itinerary

现在只能选一个主意图,后面更合理的是输出:

1
2
3
4
5
6
7
8
9
10
11
12
{
"intents": [
{
"intent": "memory",
"priority": 1
},
{
"intent": "itinerary",
"priority": 2
}
]
}

然后由流程决定是串行处理、追问确认,还是只执行其中一个。

2. 引入 Slot Filling,而不是只判断 intent

现在已经让模型分类结果里可以带 slots,但业务上还没有充分利用。

这一步其实就是意图识别里很关键的 Slot Filling

Intent 解决的是:

1
用户想做什么?

Slot Filling 解决的是:

1
2
这件事需要哪些关键信息?
这些信息现在齐了吗?

比如用户说:

1
把这个加到第 3 天

只识别出 itinerary 还不够,还要抽出:

1
2
3
4
5
6
7
8
9
{
"intent": "itinerary",
"slots": {
"day": 3,
"target": "这个",
"write_action": true,
"missing": ["具体要加入的内容"]
}
}

再比如:

1
这篇西班牙攻略加入资料 https://example.com

应该得到:

1
2
3
4
5
6
7
8
9
{
"intent": "memory",
"slots": {
"url": "https://example.com",
"destination": "西班牙",
"save_action": true,
"material_type": "攻略"
}
}

这样系统不只是知道用户“想保存资料”,还能知道保存什么、是否和当前行程有关、缺不缺关键字段。

如果关键槽位缺失,就应该进入:

1
clarify

而不是继续执行。

3. 把正则从「业务判断」降级成「候选召回」

现在有些正则还承担最终判断职责。后面应该把它们改成候选召回。

也就是说,正则不再直接决定最终意图,而是先召回可能的候选:

1
2
3
4
5
6
7
8
命中“酒店/住宿/住哪”
=> 候选:hotel、general

命中“加入资料/保存”
=> 候选:memory、clarify

命中“第 3 天/今天”
=> 候选:day_query、itinerary

然后再结合上下文、槽位完整度、模型复判,决定最终走哪条链路。

这样正则的角色会更轻,也更可维护。

4. 把规则拆成可配置的意图词表

现在规则主要写在代码里,改起来快,但长期会膨胀。

后面可以把它拆成配置:

1
2
3
4
5
6
7
8
9
10
11
{
"hotel": {
"keywords": ["酒店", "入住", "退房", "住哪", "住宿"],
"negativeKeywords": ["酒店股票", "酒店集团财报"],
"requiredSignals": ["travel_scope"]
},
"memory": {
"keywords": ["攻略", "游记", "加入资料", "保存"],
"requiredSignals": ["url", "travel_context"]
}
}

这样新增表达不用一直改代码,也更容易做评测和版本管理。

5. 用 Embedding 或检索替代一部分正则

有些表达靠关键词很难覆盖,比如:

1
2
3
4
这个适合带老人去吗
这条路线会不会太累
我们住那边方便吗
这篇先放着回头看

这类更适合用相似样本召回。

比如系统里沉淀一些已标注样本:

1
2
3
这个适合带老人去吗 -> general, slots: { topic: "suitability" }
这条路线会不会太累 -> general, slots: { topic: "route_intensity" }
这篇先放着回头看 -> clarify 或 memory

新输入来了以后,先用 Embedding 找相似样本,再把这些样本作为 few-shot 给便宜模型。

这比无限补正则更自然。

6. 做更细的澄清策略

现在 clarify 只是一个意图。

但追问本身也可以分类型:

1
2
3
4
5
6
缺少旅行语境
缺少目标日期
缺少写入确认
缺少附件类型
多个意图冲突
槽位不完整

不同原因应该问不同的问题。

比如:

1
https://example.com 加入资料

应该问:

1
这个链接和当前旅行有什么关系?是攻略、酒店、门票,还是其他参考资料?

而不是泛泛地说「请补充信息」。

7. 建一套意图识别评测集

现在主要靠手动测试。

后面应该把典型输入沉淀成测试集,例如:

1
2
3
4
5
6
股票买哪个 -> out_of_scope
第 3 天干嘛 -> day_query
这个地方安全吗 -> general
https://example.com 加入资料 -> clarify
这篇西班牙攻略加入资料 https://example.com -> memory
机酒帮我整理一下 -> flight + hotel

每次改规则或 Prompt,都跑一遍评测。

否则意图识别很容易出现「修好一个,弄坏另一个」的情况。

8. 记录准确率、延迟和成本

意图识别不是只看准不准,还要看:

1
2
3
4
5
6
7
8
规则命中率
模型调用率
平均延迟
单次成本
clarify 比例
out_of_scope 比例
模型采纳率
槽位缺失率

如果模型调用率太高,说明规则层不够好。

如果 clarify 太多,说明系统太保守。

如果 memory 误判高,说明 URL 和保存动作的边界还要继续收紧。

如果槽位缺失率高,说明很多意图不该直接执行,而应该先追问。

这些指标会比单次 Debug 更有价值。

如果把大模型看成发动机,意图识别就是方向盘。

没有方向盘,发动机越强,跑偏得越快。

一个好的意图识别系统,不是追求一次判断永远正确,而是要做到:

1
2
3
4
5
明确的,快速处理
模糊的,谨慎追问
越界的,及时拒绝
复杂的,交给模型辅助
执行的,始终由代码兜底

Agent 开发中,一部分还是传统的工程,一部分是让渡给大模型,这个度的把握也挺有意思。测试也变得困难起来,因为面对的输入变成了无穷多,不再像之前一样把固定链路测了即可。

文章中大部分知识都是看极客时间的「AI Agent 系统设计面试现场」,这里也推荐下,感兴趣的同学也可以看下,比我这里提到的会更系统。

https://coursesub.top/ 这里去买的话可以省 18 元。

windliang wechat