gradio中聊天机器人消息处理方式的祥解
官方的代码示例:

import gradio as gr
import random
import time

with gr.Blocks() as demo:
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.Button("Clear")

    def user(user_message, history):
        return "", history + [{"role": "user", "content": user_message}]

    def bot(history):
        bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
        time.sleep(2)
        history.append({"role": "assistant", "content": bot_message})
        return history

    msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
        bot, chatbot, chatbot
    )
    clear.click(lambda: None, None, chatbot, queue=False)

demo.launch()

为什么两个函数对消息的处理方式不同,user使用history + [{“role”: “user”, “content”: user_message}],而bot使用history.append({“role”: “assistant”, “content”: bot_message}),本文将给出详细的解释。
#理解一:
关键点:history 本身是列表 (list),里面的每一项是字典 {"role": "...", "content": "..."}

# user函数
return "", history + [{"role": "user", "content": user_message}]

这里 history + [字典]

  • [] 代表把这个字典包装成只有一个元素的列表
  • + 是列表拼接运算,只能列表 + 列表
  • 含义:原列表 history,拼接上新的单项列表,生成新列表,不会修改原来的 history
# bot函数
history.append({"role": "assistant", "content": bot_message})

append(item) 方法:往列表尾部追加一个元素 item

  • item 直接写字典:append(字典) ✅,把这个字典作为列表的新元素
  • 如果你写成 history.append( [{"role": "assistant", "content": bot_message}] )
    意思是:把这个小列表当成一个元素,塞进 history
    最终 history 结构变成嵌套:
[
  {"role":"user", ...},
  [{"role":"assistant", ...}]   # !这里变成列表嵌套,不是字典了!
]

Gradio 的Chatbot组件要求 history 数组里每一项必须是字典对象,一旦嵌套列表,渲染的时候直接类型报错。
#理解二:
1、为什么 + 拼接适合 user 函数?
python运行user函数的任务逻辑时序:用户输入消息提交Gradio, 把当前页面chatbot的历史传给user函数作为history入参,history + […] 构造出新历史,返回新列表,返回的新历史立刻更新页面 Chatbot,页面马上显示用户发的那句话,这个新生成的历史,自动作为参数传给后面 .then 的 bot()
如果 user 函数里用history.append(…)(原地修改),虽然也能改内容,但有坑:
Gradio 组件传参时存在引用共享,在链式调用场景容易出现状态错乱、脏数据,所以官方示例 user 函数优先用拼接新建列表。
重点:+是表达式,可以直接写在 return 后面,一次性返回新列表,完美适配 user 函数 “处理并返回新状态” 的定位。
2. 为什么 append() 适合 bot 函数?
python运行bot 的入参:已经是 user 函数处理好、追加完用户消息的 history 列表,append:原地在这个列表末尾添加 assistant 消息。
注意:append()本身返回None,不能直接写 return history.append (…) ❌
必须拆成两行:先 append 修改列表,再单独return history ✅
修改完成后,把被原地修改后的同一个 history 列表返回,更新前端页面,展示 bot 回复。
为什么 bot 不用+?不是不能用,是场景特性决定的
bot 函数两种写法都能跑:
写法 A(append,原地修改)

def bot(history):
    history.append({"role":"assistant", "content":"hi"})
    return history

写法 B(+ 拼接,新建列表)

def bot(history):
    history = history + [{"role":"assistant", "content":"hi"}]
    return history

两者区别:
append:复用同一个列表对象,适合流式迭代(yield 逐字输出),是 Gradio 流式聊天最常用写法
+拼接:每次创建新列表对象
核心结论:
user 阶段:我们希望输出一份全新的对话记录给页面 + 传给下一个函数,+拼接最干净,不会污染原始输入变量。
bot 阶段:拿到已经准备好的对话列表,只需要追加一条 assistant 消息,append原地修改最自然,流式场景更友好。
3. 一句话总结
user是第一步触发器:接收旧历史,生成一份全新的历史渲染页面、传递给 bot,+拼接天然适合这种「return 新对象」。
bot是后续回调:接收 user 产出好的完整历史,在这个列表上追加 bot 消息;append原地修改列表,之后 return 这个列表。
4. 易错点强调
❌ 错误写法:return history.append(…)
因为append()返回None,会直接返回 None 给 Chatbot,页面直接报错空白。
✅ 正确写法:先 append,再 return history。``

#理解三:如果写错,有哪些风险?

具体会触发的问题

风险①:链式回调 .then() 状态污染(最常见)

msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
    bot, chatbot, chatbot
)

执行流程:

  1. Gradio 把chatbot的原始 history 传给user
  2. user.append() 直接修改原始 history
  3. 然后 return history给前端更新页面,同时把同一个对象传给后面的bot()
    看起来没问题,但当并发、重试、异常中断、队列 queue 开启时,会出现:上一轮没执行完的 bot 函数,还持有这个列表引用,继续修改同一个 history,出现消息重复、对话错乱、历史串到别的会话

风险②:多次提交、快速连续发送消息,消息重复追加

快速连续点提交,因为原始列表被共享,多次回调同时操作同一个列表对象,会出现:同一条用户消息重复多次插入聊天记录。用 history + [{}] 每次生成新列表,就不会出现这个并发冲突。

风险③:Gradio 状态回退 / 重试逻辑异常

Gradio 内部有组件状态缓存、重试逻辑。
当原地修改原始对象:Gradio 缓存里的旧状态也被一并篡改,无法回退到提交前的历史
如果函数报错、页面刷新重试,历史记录会直接损坏。

风险④:和 gr.State() 一起使用时,BUG 概率暴增

如果你用gr.State(history)来保存对话状态:
gr.State保存的就是这个列表引用,任何地方 append 都会直接修改 State 里存储的数据,极易出现跨会话污染。

3. 什么时候勉强可以用?

只有简单 demo,单线程、关闭 queue,不会快速连续提交,不做并发访问,才勉强正常运行。
一旦开启queue=True、多用户访问、流式 yield、网络超时重试,大概率会出现诡异 bug。

4. 修复方案(推荐写法,规避所有风险)

严格按照官方的示例代码写,user里用+[ ],bot里用.append

一句话总结

原地append在 user 函数里,相当于直接篡改 Gradio 组件持有的原始对话对象
简单 demo 看不出问题;一旦上生产、开队列、多用户、流式输出,会出现消息重复、历史错乱、会话串数据等很难复现、很难排查的偶发 bug。
#理解四:核心结论:

user 里用 history + []生成新副本,原始对象不受改动,适合作为一次对话的起点,隔离状态
user 里直接 append原地修改原始对象,原始状态直接被改写,丢失提交前的快照,回溯 / 并发会出问题
bot 里用 append 是安全的,既节约内存,更关键是适配流式逐字输出的迭代模型
一次完整对话(user + bot)。+[] 会复制一个副本,原来的 history 不变;对状态管理、历史回溯、并发友好。append 直接修改原始 history,原始快照被覆盖,不利于回溯和并发。

  • history + [msg]:读取旧列表内容,新建一个全新列表。提交前的原始 history 对象原封不动保留。
    • 如果中间报错、需要重试、回滚,Gradio 还能拿到提交前的原始快照。
    • 多个请求并发时,各个请求操作各自独立的列表副本,互不干扰,不会互相污染。
  • history.append():直接修改传入的那个原始列表本身,提交前的快照直接消失,没有办法恢复到提交之前的状态。

「bot 属于完整对话的一部分,拿到最新状态,append 节约内存」
✅ 节约内存是附带效果,但不是选择 append 的最核心原因,核心是流式 yield

时序拆解完整链路

在这里插入图片描述

user() 执行 → 返回【全新的history副本】 → 传给 .then(bot)
  1. user 返回的 history,已经是一个新生成的独立列表,不再是页面最开始的原始对象。
  2. 这个新列表专门交给 bot 函数独占使用,没有别的地方持有它的原始快照了。
  3. bot 经常做流式输出(yield 逐字返回)
def bot(history):
    response = ""
    for chunk in llm_stream():
        response += chunk
        # 在这个已经独立的列表原地更新
        history[-1]["content"] = response
        yield history

流式场景下,需要反复修改同一条 assistant 消息,多次 yield 返回

如果每次都用 history + [],每输出一个字都创建新列表,代码写起来非常麻烦,内存开销反而更大。

👉 关键点区分:

  • user创建对话的 “快照分界点”,必须隔离原始状态,所以新建副本。
  • bot在 user 已经建好的新快照内部做增量修改,这个列表只归本次对话流程使用,没有外部引用,原地 append / 修改不会污染别的会话。

3. 一个容易踩坑的边界:bot 里也不是绝对永远安全

如果在 bot 里面把 history 传给别的异步任务、子线程,并且子线程也操作这个列表,同样会出现引用污染。
但在普通 .then() 串行调用、单线程 Gradio 队列场景:user 产出新列表 → 交给 bot 独占使用,append 是安全的。

4. 极简总结版(方便记)

  1. user 函数(入口)
    提交触发,需要保留提交前的历史快照,防止异常 / 并发污染。
    👉 history + [],新建副本,原始对象不动。
  2. bot 函数(后续串行回调)
    拿到 user 刚刚生成的全新独立列表,本次对话独占。
    原地 append / 修改,适配流式逐字输出,代码简洁。
    👉 append 是安全的。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐