我们常说“单轮问答”“多轮对话”和“Agent”,但这三个概念的边界并不总是清楚:多聊几轮就算 Agent 吗?模型调用一次工具又改变了什么?Memory、Planner 和 Tool 是否都是不可缺少的新概念?
与其继续增加术语,不如先退回到最少的对象:用户、模型、环境,以及它们之间流动的信息。本文尝试用这组对象描述三种结构。它们可以彼此叠加,真正的差别在于用户、模型与环境如何交互。
基本符号
| 符号 | 含义 |
|---|---|
| Intent,用户希望完成的目标 | |
| 用户生成请求时拥有的上下文 | |
| 模型生成输出时可见的上下文 | |
| Request,用户发给模型的请求 | |
| Answer,最终返回给用户的回答 | |
| 用户根据意图和上下文生成请求的过程 | |
| 模型根据输入和可见上下文生成输出的过程 | |
| Action,模型对外部环境发出的动作 | |
| Observation,环境返回给模型的观测 | |
| Environment,模型可以作用和观测的外部环境 |
用户上下文和模型上下文可能重合,但通常不相等:
用户知道自己的现实目标、处境和隐含动机;模型通常只能看到会话历史、System Prompt 和工具结果。很多交互问题——答非所问、意图误判、信息不足——都可以先从这两个上下文之间的差异来理解。
单轮交互:从请求到回答
用户根据意图和上下文生成请求,模型再生成回答:
当模型没有额外上下文时,令 ,可简化为:
因此, 是特例,而不是一般形式。真实系统中的模型几乎总处在某种上下文中,只是这部分经常被表达式省略了。
多轮对话:用户与模型形成反馈
第 轮仍遵循单轮关系,但请求和回答会更新双方的上下文:
这里的 和 分别表示用户侧与模型侧的上下文更新过程。
多轮对话的关键结构,是用户与模型之间出现反馈:
回答可能改变用户对问题的认识,进而改变下一轮目标,因此不要求 。多轮对话不只是把历史消息拼到 Prompt 后面,它还包含用户侧认知和意图的变化。
Agent:模型与环境形成反馈
Agent 允许模型先作用于外部环境,再根据观测继续决策:
其中, 可以是搜索、数据库查询、代码执行、文件读取或 API 调用, 是相应结果。模型可以重复产生 ,直到在第 步拥有足够信息并输出最终回答。
Agent 的关键结构,是模型与环境之间出现反馈:
因此,Agent 不必被定义成一种全新的模型。更简单的理解是:Agent 是 Model 与 Environment 的迭代交互过程。模型未必变了,变化的是模型所处的系统结构。
三类结构放在一起
| 结构 | 核心关系 | 新增机制 |
|---|---|---|
| 单轮交互 | 用户请求一次,模型回答一次 | |
| 多轮对话 | 前一轮结果进入后一轮上下文 | |
| Agent | 模型通过动作和观测与环境迭代 |
一个系统可以同时包含后两种反馈:
这也是今天常见的对话式 Agent:用户可以持续修正目标,模型也可以持续从环境获得新信息。它不是三种系统中的第四种,而是两条反馈回路的组合。
更复杂的系统
多用户、多模型和多环境只是在这套结构上增加节点或连接:
进一步,可以把整个系统写成一张动态图:
其中, 表示节点之间的信息或作用关系。Multi-Agent、Human-in-the-loop Agent 和 Autonomous Agent 都可以在这套图结构上描述。
State、Memory、Planner 和 Tool 也不必立刻提升为新的基本对象:
- State 是已有信息在某一时刻的封装。
- Memory 是 Context 的持久化来源。
- Tool 是 Environment 中接受 Action 并返回 Observation 的部分。
- Planner 是 Model 产生 Action 的策略或实现方式。
因此,当前用于描述系统的核心对象仍然可以保持为:
与 只是这些对象之间的上下文更新关系。只有当这组对象确实无法表达某种现象时,我们才需要增加新的基本对象。形式化的价值不在于把系统写得更复杂,而在于找到足以解释问题的最小结构。
一个例子:从单条消息的 Intent 还原用户请求
假设我们收集了一批用户与模型的对话,希望给其中的消息打标,并进一步还原用户真正想解决的问题。一段对话可以记为:
例如:
用户: 钱怎么还没到?
模型: 请问是退款、提现还是转账?
用户: 退款。
模型: 订单页显示退款成功了吗?
用户: 昨天就成功了。
模型: 原路退回银行卡通常还需要一段处理时间……
读完整段对话后,我们可以把请求概括为“查询退款成功后银行卡仍未到账”。但实际批量处理时,可以采用两种不同方式:一种是把每条 和 分别做 Embedding,再对单条消息聚类;另一种是先设计标签体系,再让 LLM 根据输入完成分类。
要解释两种方式的结果为什么不同,需要先分清两件事:LLM 是和 Embedding 一样只看单条消息,还是会看到完整对话;它是在发现数据中的自然分组,还是在套用一套预先设计的标签。
单条消息 Embedding:识别局部语义
Embedding 方案对每条消息分别编码:
再根据向量距离分别聚类:
在上面的例子中,各条消息可能呈现出这样的局部 Intent:
| 消息 | 单条消息可能聚出的 Intent |
|---|---|
| :钱怎么还没到? | 资金未到账 / 到账异常 |
| :请问是退款、提现还是转账? | 澄清资金类型 |
| :退款。 | 补充业务类型 / 退款主题 |
| :订单页显示退款成功了吗? | 确认处理状态 |
| :昨天就成功了。 | 补充状态与时间 |
| :原路退回银行卡通常还需要…… | 解释退款到账时效 |
这些结果并没有错。它们描述的是每条消息当下在做什么。但没有任何一条消息独立包含“退款已经成功,但银行卡仍未到账”这一完整请求。完整语义是随着 与 交替出现、双方上下文不断更新后才形成的:
单独计算 或 ,相当于在打标时没有显式带入这段对话上下文。它能发现消息级的语义相似性,却不会自动恢复被分散在多个轮次中的信息。
这里还要区分 和 的角色: 是用户在当前意图和上下文下产生的请求; 是模型在自己的上下文下产生的回答。因此,对 聚类得到的更准确说是回答 Intent,例如澄清、确认、解释或拒答,而不是用户 Intent。
标签体系加 LLM:按预设边界分类
另一种方式是先定义标签集合 ,写清标签边界和示例,再让 LLM 分类。如果同样对每条消息分别打标,可以写成:
此时 LLM 和 Embedding 处理的是相同粒度的输入,但优化目标仍然不同:Embedding 聚类寻找的是表示空间中自然接近的样本;LLM 分类则是在 定义的边界内选择标签。标签定义、示例和打标 Prompt 都属于 ,它们会主动规定哪些差异重要、哪些差异可以忽略。
例如,Embedding 可能把“钱怎么还没到?”与退款、提现、转账等消息聚成宽泛的“资金未到账”;如果预设标签要求区分具体业务,单条消息 LLM 则可能输出“信息不足”或一个兜底标签。反过来,如果标签体系只有“资金时效咨询”,LLM 会主动合并 Embedding 空间里原本可以分开的多个簇。
如果目标不是给单条消息打标,而是还原整段对话的用户请求,还可以把完整历史 一起提供给 LLM:
这时 LLM 同时看到了“未到账”“退款”“已经成功”以及各条消息的先后关系,可以把分散在不同轮次的信息组合起来,并输出“退款成功但未到账”。这里的结果又多了一层差异:它已经从消息级分类变成了对话级归纳。
为什么两边的结果不同
| 维度 | 单条消息 Embedding 聚类 | 单条消息 LLM 分类 | 完整对话 LLM 分类 |
|---|---|---|---|
| 输入 | 单个 或 | 单个 或 | 完整对话 |
| 粒度 | 消息级 Intent | 消息级 Intent | 对话级用户请求 |
| 上下文 | 不显式包含其他轮次 | 标签、定义和示例 | 标签以及对话历史、顺序、角色 |
| 目标 | 找到语义相似的消息 | 映射到预设消息标签 | 映射到预设对话标签 |
| 输出 | 数据分布形成的一串局部簇 | 标签体系规定的一串局部标签 | 一个整体标签 |
从形式上看,无论使用 Embedding 聚类还是让 LLM 逐条分类,消息级方案最终得到的都是一个标签序列:
而对话级 LLM 得到的是一个整体判断 。如果希望仅凭单条消息的打标结果还原整段请求,中间其实还缺少一个聚合过程:
不仅要合并局部标签,还要理解消息顺序和用户、模型的不同角色。这个聚合过程可以由规则、另一个模型或读取完整历史的 LLM 实现。没有 ,就不能直接拿“一串消息 Intent”与“一个对话标签”比较,并把差异归因于 Embedding 或 LLM 的能力。
回到最初的形式化描述,单条请求满足:
当 脱离 单独出现时,省略、指代和承接关系都会丢失。“退款”“昨天就成功了”在完整对话中很关键,单独看却都不足以表达最终请求。这正是 只是一种特例、一般形式必须保留 Context 的实际例子。
因此,要分两层理解结果差异:在相同的单条消息粒度下,Embedding 聚类发现的是数据中的相似结构,LLM 分类执行的是人为定义的标签边界;当 LLM 读取完整对话时,它还额外承担了上下文恢复与消息聚合。要公平比较两种方法,需要先对齐输入、上下文、粒度与标签目标。