【Bug已解决】Assistant reprocesses previous prompt. 解决方案
原始报错线索:Assistant reprocesses previous prompt.(助手把上一条 prompt 又处理了一遍,用户明明只发了一次,却得到两次响应)。
一、现象长什么样
用户发一条消息,期待一个回复,结果:
- 助手回了两次相同或几乎相同的内容;
- 或某些「动作」被执行了两次(发两封邮件、建两个任务);
- 用户确认只点了一次发送;
- 往往发生在「网络重发 / UI 重绘 / 事件冒泡」场景下------同一条输入被系统从不同路径递送了两次。 根因是输入消费没有幂等保护,且上一次处理的事件/状态残留,被当成了「新输入」再次处理。
二、背景:为什么一条输入会被处理两次
输入从「用户敲击」到「被处理」,往往经过多层:
UI 事件 → 网络传输 → 队列 → 处理器
任何一层若「重复递送」且下游不防重,就会双处理:
- UI 层:按钮连点、事件冒泡;
- 网络层:超时重试把同一条请求发两次;
- 队列层:消费确认失败,消息被重新投递;
- 状态层:上一次处理的「最后一条输入」没清,重启后又被读出来当新输入。
三、为什么重复处理:根因
3.1 输入无唯一 ID / 指纹
系统无法区分「这是同一条重发的」还是「这是新的」,于是都处理。
3.2 消费不幂等
处理器对同一条输入执行了副作用(发消息、建任务),重复执行就有重复副作用。
3.3 状态残留
「上一条 prompt」存在某处状态里,被误当成新输入再次喂给处理器。
3.4 重试无去重
网络重试把同一条带相同内容的请求发两次,下游照单全收。
四、最小可运行复现(无去重双处理)
下面演示「同内容输入被处理两次」:
python
def process_prompt_bad(prompt, state):
# 错误:直接处理,不检查是否重复
state["last_response"] = f"回复: {prompt}"
state["responses"].append(state["last_response"])
return state["last_response"]
if __name__ == "__main__":
state = {"responses": []}
process_prompt_bad("你好", state)
process_prompt_bad("你好", state) # 同内容又来一次 -> 双处理
print("响应数:", len(state["responses"])) # 2,重复了
无去重,相同内容被处理两次(根因 3.1/3.2)。
五、解决方案一:输入指纹去重
给每条输入算指纹(内容哈希 + 可选序号),已处理过的直接跳过:
python
import hashlib
class DeduplicatingProcessor:
def __init__(self):
self.seen = set()
def fingerprint(self, prompt, user_id):
# 内容 + 用户 + 时间窗(可选)构成指纹
raw = f"{user_id}:{prompt}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
def process(self, prompt, user_id, handler):
fp = self.fingerprint(prompt, user_id)
if fp in self.seen:
return None, "duplicate" # 去重,不重复处理
self.seen.add(fp)
return handler(prompt), "ok"
if __name__ == "__main__":
p = DeduplicatingProcessor()
h = lambda x: f"回复:{x}"
print(p.process("你好", "u1", h)) # ('回复:你好', 'ok')
print(p.process("你好", "u1", h)) # (None, 'duplicate') 拦截
指纹去重,同内容二次到达被拦(解决 3.1,呼应第 110/120 篇幂等)。
六、解决方案二:消费幂等(带唯一请求 ID)
从输入端就带「请求 ID」,下游用 ID 保证「同一 ID 只产生一次副作用」:
python
class IdempotentHandler:
def __init__(self):
self.processed_ids = set()
def handle(self, request_id, prompt, side_effect):
if request_id in self.processed_ids:
return "already_done" # 幂等:不重复副作用
# 执行副作用(发消息/建任务)
result = side_effect(prompt)
self.processed_ids.add(request_id)
return result
def send_message(text):
return f"已发送: {text}"
if __name__ == "__main__":
h = IdempotentHandler()
print(h.handle("req-001", "你好", send_message)) # 已发送
print(h.handle("req-001", "你好", send_message)) # already_done(重试安全)
print(h.handle("req-002", "你好", send_message)) # 已发送(不同 ID)
请求 ID 幂等,网络重试(同 ID)不重复副作用。
七、解决方案三:状态不残留上一条输入
「上一条 prompt」不该成为「下次自动处理的输入源」。处理完即清,或明确区分「历史」与「待处理队列」:
python
class InputQueue:
def __init__(self):
self.pending = [] # 仅待处理
self.history = [] # 已处理历史,不参与再处理
def enqueue(self, prompt):
self.pending.append(prompt)
def consume_one(self, handler):
if not self.pending:
return None
prompt = self.pending.pop(0) # 取出即移除,不残留
result = handler(prompt)
self.history.append(prompt) # 进历史,但历史不会被再消费
return result
if __name__ == "__main__":
q = InputQueue()
q.enqueue("你好")
print(q.consume_one(lambda x: f"回复:{x}")) # 回复:你好
print("待处理剩余:", q.pending) # [] 不残留
print(q.consume_one(lambda x: x)) # None(不会把历史当新输入)
待处理队列「取出即移除」,历史与待处理分离,杜绝残留重处理。
八、跨层防重策略
- UI 层:按钮防连点(debounce / disable until response);
- 网络层:重试带相同请求 ID,下游幂等;
- 队列层:消费确认(ack)后才删消息,失败重投但下游幂等;
- 处理器层:指纹去重 + ID 幂等(第五、六节);
- 可观测:记录「重复拦截次数」。
九、排查清单
「助手重复处理上一条 prompt」,按下面排查:
- 输入有无指纹/请求 ID?能否区分重复(第五、六节);
- 消费是否幂等?重复执行是否有重复副作用(第六节);
- 上一条输入是否残留?待处理与历史是否分离(第七节);
- UI 是否防连点;
- 网络重试是否带相同 ID;
- 队列是否确认消费(ack 机制);
- 日志是否记重复拦截数;
- 是否只在特定场景(重发/重绘)出现?定位递送层。
十、小结
「助手重复处理上一条 prompt」的根因是输入无去重、消费不幂等、且上一条输入状态残留被再处理。通用修复:
- 指纹去重:内容哈希构成指纹,已处理直接跳过(第五节,呼应第 110/120 篇);
- 请求 ID 幂等:带唯一 ID,同 ID 只产生一次副作用,重试安全;
- 状态不残留:待处理队列「取出即移除」,历史与待处理分离;
- 跨层防重 :UI 防连点 + 网络带 ID + 队列 ack + 处理器幂等(第八节)。 一句话:一条输入只要「带唯一身份 + 下游幂等消费 + 处理完即从待办移除」,就绝不会被处理两次。把去重和幂等做成输入链路的标配,助手就不会再把你的「你好」回答两遍------这与第 110 篇幂等记账、第 120 篇幂等恢复、第 129 篇重试安全,共同体现「任何会被重复递送的输入,都必须有去重与幂等保护」。
