Agent 用户连续输入解决
这是个非常真实、而且必须在架构层解决的问题。
你这个场景本质上不是“LLM 回复慢”,而是:
用户输入流是异步连续到达的,但 Agent 工作流是有状态、长执行链的。
尤其你用的是 LangGraph,它本身就适合做这类有状态、多步骤、可中断的 Agent,所以这个问题应该在会话编排层处理,而不是只在前端“禁用输入框”。
一、先说结论
对于导购智能体,用户在服务端未回复完时连续发消息,不要简单按“每条消息都立刻启动一个新图”处理,否则一定会乱。
正确做法通常是:
核心原则
- 一个会话 thread 只允许一个 active graph run
- 新消息到达时,先进入 消息缓冲区(inbox / queue)
- 根据策略判断:
- 打断当前执行
- 合并到当前轮次
- 排队等当前执行完成后再处理
- 对“补充说明 / 修正前文 / 否定前文”这类消息,要优先视为 override / amend(覆盖/修正)
- 对“新问题”这类消息,可以排队成下一轮 turn
一句话:
你要在 LangGraph 外面加一层“会话调度器(Conversation Orchestrator)”。
二、为什么这是个大问题?
假设用户先发:
“我想买个送老婆的生日礼物,预算 2000。”
Agent 正在:
- 识别旅程:探索期
- 做推荐
- 调商品检索工具
这时用户又发:
“不是老婆,是送妈妈。”
“而且预算 1000 以内。”
“最好实用一点。”
如果你不做控制,会发生这些灾难:
灾难 1:并发两个 graph run
第一个 run 还在按“送老婆、预算 2000”推荐。
第二个 run 又按“送妈妈、预算 1000”跑。
最后两个回复交错回来,用户直接懵。
灾难 2:状态污染
第一个 run 把 state 里写入:
- recipient=wife
- budget=2000
第二个 run 又写:
- recipient=mother
- budget=1000
如果没做好版本控制,state 会互相覆盖。
灾难 3:工具浪费
前一个 run 可能已经调用了 RAG、推荐 API、库存服务。
但这些已经不再有意义。
三、最推荐的总体架构
你可以把整个系统分成 4 层:
[1] WebSocket / IM 接入层
↓
[2] 会话调度器 Conversation Orchestrator
↓
[3] LangGraph Agent Runtime
↓
[4] 工具层 / RAG / 推荐系统 / 下单系统
其中最关键的是第 2 层。
四、会话调度器要做什么?
它负责管理每个用户会话的这几个东西:
1. 当前是否有运行中的 graph
比如:
thread_id = user_123_session_001active_run_id = run_abcstatus = RUNNING
2. 消息缓冲区
例如:
[
{"msg_id":"m2","text":"不是老婆,是送妈妈","ts":1711111111},
{"msg_id":"m3","text":"预算1000以内","ts":1711111112},
{"msg_id":"m4","text":"最好实用一点","ts":1711111113}
]
3. 当前轮次状态
例如:
- 当前处于探索期
- 当前正在做推荐
- 当前是否已经调用了工具
- 当前回复是否可中断
4. 中断策略
决定新消息来了以后怎么处理。
五、你需要定义 3 种新消息处理策略
这个最核心。
策略 A:Append(追加)
适用于:
- 用户只是补充一点细节
- 不改变当前任务主方向
- 当前 run 即将完成,不值得打断
例如:
“最好轻一点”
“颜色别太花”
这种可以先放入 pending queue,等当前回复结束后,下一轮再合并处理。
策略 B:Amend / Merge(修正合并)
适用于:
- 用户是在补充和修正前文
- 本质上还是同一个任务
- 但约束发生变化
例如:
“不是送老婆,是送妈妈”
“预算不是 2000,是 1000”
“不是手机,是耳机”
这类消息应该被识别为:
对当前用户意图的修正
此时建议:
- 标记当前 run 为 stale(过期)
- 触发中断
- 把最近几条连续消息做 merge,生成新的 user turn
- 用新的 merged input 重跑 graph
策略 C:Fork / Next Turn(下一轮)
适用于:
- 用户发的是一个新问题
- 和当前任务不完全一致
- 不一定要中断当前流程
例如:
“对了,你们支持花呗吗?”
“顺便问下这个多久发货?”
这类有两种处理方式:
- 如果当前已进入决策期,可作为下一轮继续
- 如果是关键交易问题,也可以中断并切换
六、最实用的判断标准:把新消息分成 4 类
建议你在会话调度器里做一个轻量分类器,把新消息分成:
1. correction(纠正)
典型词:
- 不是
- 改成
- 说错了
- 更正一下
- 不对
- 应该是
例子:
- “不是送老婆,是送妈妈”
- “预算改成1000”
2. supplement(补充)
典型词:
- 另外
- 还有
- 补充一下
- 最好
- 顺便说下
例子:
- “最好适合 50 岁女性”
- “最好别太贵”
3. cancel / restart(取消/重来)
典型词:
- 算了
- 不用了
- 重新推荐
- 换一种
- 重来
例子:
- “算了,重新推荐吧”
- “不要这个方向了”
4. new_intent(新意图)
例子:
- “怎么退货?”
- “这个能开发票吗?”
七、推荐的处理逻辑
你可以设计成这样:
如果当前没有 active run:
直接启动 graph
如果当前有 active run:
对新消息做分类
- correction / cancel:中断当前 run,合并消息,重启
- supplement:短暂缓冲(debounce),合并后决定是否重启
- new_intent:进入 next_turn_queue
八、为什么要加 debounce(防抖)?
因为用户连续发消息时,通常不是只发一条,而是会连发 2~5 条。
比如:
“不是老婆”
“是妈妈”
“50岁左右”
“预算1000”
“最好实用”
如果你每来一条都立即中断并重跑 graph,系统会疯狂抖动。
所以建议你做一个 300ms ~ 1500ms 的短暂聚合窗口。
推荐做法
当检测到用户在连续输入时:
- 第一条新消息到达:先不立刻重跑
- 开一个短 timer,比如 800ms
- 如果期间又来了新消息,就继续收集
- 800ms 内无新消息,再把这批消息 merge 成一个“修正版输入”
比如最后合并成:
“更正:不是送老婆,是送妈妈,50岁左右,预算1000以内,最好实用。”
然后只触发一次 graph restart。
九、在 LangGraph 里怎么落地?
LangGraph 本身擅长:
- 有状态图
- 持久化 state
- checkpoint
- interrupt/resume
- thread 级执行
所以你可以这样设计。
1. state 设计
你的会话 state 不只是 messages,还要有运行元信息:
class ShoppingState(TypedDict):
messages: list
user_profile: dict
journey_stage: str
current_intent: str
current_slots: dict
active_run_id: str
run_version: int
pending_user_msgs: list
is_interrupted: bool
2. 每次 run 带 version
比如:
- 第一次运行:
run_version = 1 - 用户修正后重启:
run_version = 2
这样 graph 节点在执行工具调用后,可以检查自己是否还是最新版本。
例如:
- 节点启动时记录 version=1
- 调完推荐 API 回来后发现全局 version 已经变成 2
- 说明自己过期了,直接放弃输出,不再写回最终回复
这很重要。
因为就算你中断了,有些异步工具调用可能已经发出去了,回包还是会回来。
3. 中断点设计
LangGraph 里不要等整个图跑完才有机会响应新消息。
建议在这些节点后设置“可中断检查”:
- 意图识别后
- 工具调用前
- 工具调用后
- 最终生成前
也就是每个关键节点都检查一次:
if state["run_version"] != runtime_context.current_run_version:
return STOP_CURRENT_RUN
4. 输出流式回复时的处理
如果你是流式返回(streaming),用户在模型还在生成时又发新消息,这时建议:
推荐策略
- 立即停止当前 streaming
- 前端展示:
- “已根据你的新补充调整推荐”
- 后端开始新 run
否则用户会看到:
- 前半段在讲送老婆
- 后半段你又切到送妈妈
体验很差。
十、消息合并要怎么做?
建议不要简单字符串拼接。
应该做“语义合并”。
简单版
用一个小模型/LLM,把连续消息整理成一条结构化修正版输入:
输入:
原始需求:我想买个送老婆的生日礼物,预算2000
后续补充:
1. 不是老婆,是送妈妈
2. 预算1000以内
3. 最好实用一点
输出:
{
"task": "gift_recommendation",
"recipient": "mother",
"age_hint": "50+",
"budget": "1000以内",
"style": "实用",
"replace_previous_constraints": true
}
然后再喂给主 graph。
工业版
直接维护 slot state:
- recipient
- age_group
- budget
- style
- category
- scene
新消息到来时,不是重写整段 messages,而是更新 slots。
例如:
{
"recipient": "wife" -> "mother",
"budget": 2000 -> 1000,
"style": null -> "practical"
}
这对于导购 Agent 很适合,因为它本来就有结构化槽位。
十一、推荐你用“消息层”和“业务状态层”分离
这是重点。
不要把所有逻辑都依赖聊天消息历史。
要分成两层:
1. 消息层
保存原始消息历史,便于审计和回溯。
2. 业务状态层
保存当前导购上下文:
- 用户旅程
- 当前商品实体
- 预算
- 收礼对象
- 风格偏好
- 风险偏好(如果是金融)
- 当前候选商品列表
这样用户后续补充时,你只更新业务状态,不用每次重放全部对话。
十二、给你一个推荐的状态流转方案
场景 1:正在推荐时,用户修正前文
用户:
- “送老婆,预算2000”
- Agent 正在推荐
- 用户又发:“不是老婆,是送妈妈,预算1000”
处理:
- 识别为 correction
- 中断当前 run
- 合并最近补充消息
- 更新 slots
- 启动新 run
- 前端显示:“收到,我按送妈妈、预算1000重新推荐”
场景 2:正在推荐时,用户补充偏好
用户:
- “推荐个耳机”
- 过 1 秒又发:“最好降噪强一点”
处理:
- 识别为 supplement
- 若当前 run 还没进入最终生成,可选择中断重跑
- 若当前 run 已快结束,可把补充作为 next turn,紧接着答
场景 3:正在答一个问题,用户换话题
用户:
- 正在问礼物推荐
- 又发:“对了,订单多久能到?”
处理:
- 识别为 new_intent
- 如果优先级高,可插队
- 否则排到 next_turn_queue
十三、前端交互也要配合
后端再强,如果前端没有交互提示,体验也会乱。
建议前端支持这些状态:
1. “正在思考”
显示 Agent 正在处理。
2. “已收到你的补充,正在调整”
当用户在 streaming 中追加消息时,给明确反馈。
3. “已中断上一轮推荐,按最新信息重新处理”
让用户知道系统没有忽略他的新消息。
十四、一个比较推荐的 LangGraph 外围实现方式
你可以在 LangGraph 外包一个 SessionManager。
伪代码
class SessionManager:
def __init__(self):
self.sessions = {}
def on_user_message(self, thread_id, message):
session = self.sessions[thread_id]
if not session.active_run:
self.start_new_run(thread_id, [message])
return
msg_type = classify_message(message, session)
if msg_type in ["correction", "cancel"]:
session.pending_msgs.append(message)
session.mark_interrupt = True
self.interrupt_and_restart(thread_id)
elif msg_type == "supplement":
session.pending_msgs.append(message)
self.schedule_debounce_restart(thread_id)
else: # new_intent
session.next_turn_queue.append(message)
restart 逻辑
def interrupt_and_restart(self, thread_id):
session = self.sessions[thread_id]
session.run_version += 1
stop_current_run(session.active_run_id)
merged_input = merge_messages(session.pending_msgs)
session.pending_msgs = []
self.start_new_run(thread_id, [merged_input])
十五、最重要的工程建议
1. 不要让同一 thread 并发跑多个主 graph
这是第一原则。
2. 任何异步工具返回都要检查 run_version
否则旧结果会污染新状态。
3. 对连续消息做 debounce 聚合
否则系统会频繁抖动。
4. 做 correction / supplement / new_intent 分类
不同类型处理方式不同。
5. 导购场景优先维护结构化 slots
因为很多连续消息本质是在修正 slots。
十六、最适合导购 Agent 的一句架构总结
对于导购智能体,用户连续发消息时,不要把每条消息都当成独立 turn,而要把它们看成:
对当前购买任务的动态修订流
所以你的系统应该是:
- 消息级接收
- 会话级调度
- 状态级合并
- 图级重启/中断
而不是“来一条跑一次 LangGraph”。
如果你愿意,我可以下一步直接给你两样特别实用的东西:
- 基于 LangGraph 的会话调度架构图
- 一套可直接落地的 Python 伪代码/代码骨架
- SessionManager
- 消息分类器
- debounce
- run_version 中断机制
- LangGraph state 定义
如果你要落地,我建议我下一条直接给你 代码骨架版。

浙公网安备 33010602011771号