2026-08-31

关于意图识别的思考 ---- Plan + 工具设计

我做了很多轮意图识别,最后发现:问题可能根本不在意图识别最近我一直在迭代「安排日历」里的意图识别。

一开始,我以为这只是一个很普通的分类问题:

用户说一句话,模型判断他想做什么。

比如:

  • “明天下午三点帮我开个会” → 创建日程
  • “我明天有什么安排?” → 查询日程
  • “把下午三点的会议取消掉” → 删除日程
  • “改到四点吧” → 修改日程

看起来并不复杂。

但真正做进产品之后,我发现一个很麻烦的问题:

Bad Case 永远修不完。

修好了“明天下午开会”,换成“明天下午帮我留两个小时”可能又不行。

修好了单轮对话,多轮澄清又出了问题。

修好了意图分类,模型知道用户想创建日程,却又不一定真的调用创建日程的 Tool。

于是整个开发过程逐渐变成:

发现一个 Case → 改 Prompt → 测试 → 修好 → 又出现新的 Case → 再改 Prompt……

做了很多轮以后,我开始意识到:

这可能已经不是一个 Prompt Engineering 问题,而是一个 Agent 架构问题。


一、真正的意图,往往不在用户最后一句话里

日历是一个非常典型的多轮交互场景。

例如用户说:

帮我安排一个产品讨论。

系统不能直接创建,因为缺少时间。

于是 Agent 问:

你希望安排在什么时候?

用户回答:

明天下午吧。

Agent继续问:

下午几点?

用户回答:

三点。

如果只看最后一句“三点”,几乎无法判断用户到底想干什么。

真正的用户意图其实分布在整个 Conversation History 中:

User: 帮我安排一个产品讨论
Assistant: 你希望安排在什么时候?
User: 明天下午吧
Assistant: 下午几点?
User: 三点

最终我们需要恢复出来的其实是:

Action: CreateEvent

Title: 产品讨论
Date: 明天
Time: 15:00

所以我现在越来越倾向于:

不要过早把每一句 User Message 独立分类。

应该把最近一段 Conversation History 连同当前输入一起交给模型,让模型判断:

用户当前到底想完成什么事情?

换句话说,意图识别的输入不是:

User Message

而应该是:

Conversation Context
+
Current User Message
+
System Context

这是第一个变化。


二、不要让“意图识别”成为整个系统的中心

我最开始的思路其实比较传统。

模型负责输出一个 JSON:

{
  "intent": "create_event",
  "title": "产品讨论",
  "date": "tomorrow",
  "time": "15:00"
}

然后程序根据 JSON:

LLM

Intent JSON

Business Code

Calendar API

这个架构非常容易理解。

但随着能力越来越复杂,我发现它开始变得笨重。

因为真实世界里的用户请求不是几个 Enum 可以完全描述的。

比如:

明天下午找个我和 Alex 都有空的时间聊一下项目,大概一个小时。

这里至少包含:

理解意图

解析日期

查询我的 Calendar

查询 Alex 的 Availability

寻找共同空闲时间

创建 Event

这时候所谓的 Intent 已经不再只是:

CREATE_EVENT

而是一系列需要完成的 Action。

所以我后来逐渐把架构调整成了:

Conversation

     LLM

   Agent Loop
   ↙   ↓   ↘
Query  Create  Update ...
Calendar Event Event

Intent 不再一定需要成为一个显式的中间产物。

模型理解了用户想做什么之后,可以直接决定下一步应该调用什么 Tool。

这其实是从:

Intent Classification

逐渐走向:

Agent Decision Making。


三、把日历能力变成 Tools,而不是无限扩充 Intent

于是我开始把日历的基础能力注册成 Tools。

最基本的是 CRUD:

query_calendar
create_event
update_event
delete_event

每个 Tool 都有明确的:

name
description
parameters
return value
error

例如:

create_event(
  title,
  start_time,
  end_time,
  participants,
  ...
)

Agent拿到这些能力之后,就可以自己决定什么时候调用。

例如:

帮我看看明天下午有没有空。

Agent:

→ query_calendar()
→ 返回结果
→ 给用户回答

而:

明天下午三点帮我安排一个产品讨论。

则是:

→ create_event(...)
→ 返回成功
→ 告诉用户已经创建

如果信息不完整:

帮我安排一个产品讨论。

Agent不调用任何 Tool,而是直接回答:

你希望安排在什么时候?

用户:

明天下午三点。

第二轮模型重新拿到 Conversation Context:

User: 帮我安排一个产品讨论
Assistant: 你希望安排在什么时候?
User: 明天下午三点

此时信息完整:

→ create_event(...)

这套机制最大的变化在于:

澄清本身也是 Agent 行为的一部分,而不需要单独设计一个巨大的状态机。


四、Tool Calling 失败,也不意味着 Agent 失败

接下来又出现了另一个现实问题。

Tool 会失败。

而且失败原因很多。

比如模型调用:

create_event(...)

结果可能返回:

INVALID_ARGUMENT

也可能:

TIME_CONFLICT

甚至:

DATABASE_TIMEOUT

这三种错误虽然都是“Tool 调用失败”,但处理策略完全不同。

所以我现在更倾向于把 Tool Error 做结构化分类。

例如:

Tool Error

    ├── Parameter Error
    │       ↓
    │    Agent Retry

    ├── Business Error
    │       ↓
    │    Agent Reasoning

    └── System Error

       Fail + Observability

参数错误

比如:

start_time format invalid

这种错误完全可以重新返回给模型。

模型可能发现:

原来 Tool 要的是 ISO 8601。

于是重新生成参数并 Retry。

不需要用户知道第一次调用失败了。


业务错误

例如:

TIME_CONFLICT

这不是系统故障。

Agent可以继续处理:

创建日程

发现冲突

查询附近空闲时间

给用户推荐替代时间

甚至未来可以进一步:

这个时间和你的周会冲突。16:00 和 16:30 都有空,需要帮你改到其中一个吗?

这实际上已经进入真正的 Agent Reasoning。


系统错误

例如:

Database Timeout
Internal Server Error
Network Error

这种问题模型自己解决不了。

此时应该:

停止 Retry
+
返回用户可理解的信息
+
写入 Observability
+
触发监控

然后由开发者根据 Trace 定位。

所以:

Agent Retry 不是万能重试,而应该建立在 Error Taxonomy 之上。


五、做到这里以后,我又发现了一个问题:ReAct 会漂移

如果整个系统只是:

Think

Act

Observe

Think

Act

Observe

也就是经典 ReAct Loop,那么简单任务很好用。

例如:

创建明天下午三点的会议。

可能只需要一次 Tool Call。

但任务复杂以后,问题就出现了。

比如:

把我明天下午所有会议往后推一个小时,如果有冲突就找附近的空闲时间,重要会议不要动。

Agent可能需要:

查询日历
→ 判断哪些会议可以修改
→ 修改 A
→ 查询冲突
→ 修改 B
→ 重新查询
→ 修改 C
→ ...

执行链一旦变长,模型很容易逐渐偏离用户最开始的目标。

这就是我们今天讨论到的一个很重要的问题:

ReAct Loop 很容易发生 Goal Drift。

模型每一步都在根据最新 Observation 做决定。

但是走了五六步之后,它可能已经把最初的约束忘掉了一部分。

比如:

“重要会议不要动。”

执行到后面可能就被弱化了。

于是我们聊到了 Planner。


六、Planner 的三种实现

目前我认为比较典型的有三种。

方案一:Planner 内置在 Agent Loop

第一轮强制生成 Plan:

User

Plan

ReAct

Tool

Observation

ReAct

例如:

Goal:
调整明天下午的会议

Plan:
1. 查询明天下午所有会议
2. 判断哪些会议允许移动
3. 每个会议顺延 1 小时
4. 检查冲突
5. 冲突时寻找最近空闲时间
6. 不修改重要会议

之后整个 ReAct Loop 都带着这个 Plan。

它最大的价值就是:

不断把 Agent 的注意力拉回原始目标。


方案二:独立 Planner Agent

把 Planner 和 Executor 完全拆开:

             ┌─────────────┐
User ───────→│ Planner     │
             └──────┬──────┘

                  Plan

             ┌─────────────┐
             │ Executor    │
             └──────┬──────┘

                  Tools

Planner 负责:

What should be done?

Executor 负责:

How to execute it?

这种方式职责非常清晰,也非常适合复杂 Agent。

但代价也明显:

多一次甚至多次模型调用。

Latency、Token Cost、系统复杂度都会增加。

对于“明天下午三点提醒我开会”这种请求,显然没有必要。


七、第三种方案反而是我目前最喜欢的:Planner as a Tool

还有一种很有意思的设计:

把 Planner 本身注册成一个 Tool。

例如:

Tools:
- query_calendar
- create_event
- update_event
- delete_event
- create_plan

Agent收到:

明天下午三点提醒我开会。

它发现非常简单:

create_event()

直接执行。

根本不需要 Planner。

但如果用户说:

把我下周所有和项目 A 有关的会议重新整理一下,尽量集中到周二和周三,但不要影响已经确认的重要会议。

模型判断:

这个任务比较复杂。

于是:

create_plan()

得到:

1. 查询下周会议
2. 筛选项目 A
3. 标记不可移动会议
4. 查询周二、周三空闲时间
5. 生成迁移方案
6. 检查冲突
7. 请求用户确认
8. 执行修改

然后再进入 ReAct Loop。

于是整个架构变成:

                 User

                 Agent
              ↙     ↓     ↘
         Simple   Complex   Clarify
           ↓        ↓         ↓
         Tool    Planner    User

                  Plan

                  ReAct

                  Tools

我现在比较喜欢这个方案的原因是:

简单任务保持简单,复杂任务才支付 Planning 的成本。

Planner 从 Agent 的固定流程,变成了一种按需调用的认知能力。


八、未来还可以继续演进:Hierarchical Planning

如果以后它不再只是一个 Calendar Agent,而逐渐成为一个 Life Assistant,那么 Planning 可能还需要继续分层。

比如用户说:

我希望三个月以后可以参加一次半程马拉松。

这已经不是 Calendar CRUD 了。

系统可能需要把它拆成:

Goal

Plan

Tasks

Calendar Actions

例如:

完成半程马拉松

制定 12 周训练计划

每周训练 4 次

寻找用户空闲时间

创建具体日程

这时候:

Goal Planner

Task Planner

Calendar Agent

Tools

Planner 自然就会从一个简单 Tool,逐渐演进成 Hierarchical Planner。

但这是后面的事情。

我越来越觉得,Agent 架构有一个很重要的原则:

不要一开始就把最终架构全部造出来,而应该让复杂度随着问题出现逐层生长。


九、重新看“意图识别”:也许它根本不是一个分类器

走到这里,再回头看最开始的问题,会发现很有意思。

一开始我想解决的是:

如何提高意图识别准确率?

所以我不断增加:

Intent
Prompt
Rules
Examples
Few-shot
Bad Case

但是现在我越来越觉得:

对于 Agent 产品来说,所谓的“意图识别”可能根本不应该只是:

User Input

Intent Classifier

CREATE / QUERY / UPDATE / DELETE

更合理的结构可能是:

Conversation Context

    Agent Reasoning

 ┌────────┼─────────┐
 ↓        ↓         ↓
Answer  Clarify   Action

              ┌────┴────┐
              ↓         ↓
            Simple    Complex
              ↓         ↓
            Tool      Planner

                      ReAct

                      Tools

Intent Recognition 从一个独立模块,逐渐变成了 Agent Reasoning 的一部分。

这也是我最近做日历 Agent 最大的一个认知变化。


十、Bad Case 修不完的时候,也许应该停下来看看架构

做 AI 产品很容易陷入一种状态:

出现 Bad Case,就继续改 Prompt。

这当然有用。

但如果你发现:

一个 Case 修好了,换一种表达方式又坏了。

而且这种事情不断发生,那么值得停下来问一个问题:

我到底是在解决模型能力问题,还是在用 Prompt 弥补架构问题?

这是两件完全不同的事情。

我现在给自己的一个判断标准是:

如果一个问题需要不断增加:

if 用户这样说……
如果上一轮这样……
如果这个字段不存在……
如果发生冲突……
如果 Tool 失败……
如果……

那么很可能意味着:

这里应该出现一个新的架构抽象了。

它可能是:

Tool
Memory
Planner
State
Workflow
Error Taxonomy
Observability

而不是 Prompt 里的第 87 条规则。


写在最后

最近做 Agent 越来越强烈的一个感受是:

Agent 工程真正困难的地方,并不是让 LLM “更聪明”,而是设计一个架构,让 LLM 即使偶尔不聪明,系统仍然能够稳定工作。

Conversation 帮它恢复上下文。

System Prompt 给它边界。

Tools 给它行动能力。

Structured Error 给它纠错能力。

Planner 帮它保持目标。

Observability 帮开发者发现系统性问题。

Memory 则让它逐渐理解这个用户。

最后形成的其实已经不是一个所谓的“意图识别模块”。

而是:

Context

Reasoning

Planning

Action

Observation

Recovery

Memory

这是一个完整的 Agent Loop。

所以现在如果再让我回答:

“日历 Agent 的意图识别应该怎么做?”

我的答案可能会变成:

不要只做意图识别。

让 Agent 理解用户正在完成的事情,给它足够清晰的工具、约束、Planning 和错误恢复机制,然后让“意图”自然地体现在它下一步选择的 Action 里。

这可能比维护一个越来越庞大的 Intent Classifier,更接近 Agent 产品最终应该有的样子。

评论

加载中…
评论通过 GitHub 安全存储,提交后稍等片刻显示。