LangChain 之 多模态聊天机器人实战
AI大模型工程师-应用篇 系列目录:
├── LangChain
│ ├── 提示词模板、对话管理与结构化输出核心用法
│ ├── LCEL 表达式语法
│ └── 多模态聊天机器人实战(本文)
├── Embedding
│ └── Embedding 与向量数据库简单应用——从文本向量化到 RAG 检索增强生成
├── LangGraph
│ └── LangGraph 工作流与 Agent 开发实战
└── MCP
└── 协议与 Agent 通信实战(即将发布)
本文从零开始,逐步构建一个支持文本、音频、图像多模态输入的聊天机器人。在实战过程中,将综合运用 LangChain 的对话记忆管理(InMemory / SQLite)、历史摘要策略、LCEL 链式编排,并结合 Gradio 搭建交互式 Web 界面,完整展示一个 AI 应用从后端逻辑到前端交互的全链路开发过程。
文章目录
一、项目概述与依赖准备
1.1 我们要构建什么
本文将分步构建一个多模态聊天机器人,最终形态支持:
- 文本对话:基础问答 + 多轮上下文记忆
- 对话持久化:聊天记录存储到 SQLite 数据库,重启不丢失
- 历史摘要:自动压缩过长的对话历史,避免 Token 溢出
- 语音输入:通过 ASR 语音识别将语音转为文字
- 图像 / 音频原生理解:借助多模态大模型,直接处理图片和音频
整体技术栈:
| 层级 | 技术选型 |
|---|---|
| 链路编排 | LangChain LCEL |
| 对话记忆 | RunnableWithMessageHistory + SQLChatMessageHistory |
| 历史管理 | RunnablePassthrough.assign + 摘要链 |
| Web 界面 | Gradio |
| 语音识别 | ZhipuAI ASR(glm-asr) |
| 多模态处理 | Base64 编码 + 多模态 LLM |
1.2 额外依赖
本文使用 uv 作为 Python 包管理工具。将以下内容保存为项目根目录下的 pyproject.toml 文件,然后在终端执行 uv sync,即可自动创建虚拟环境并安装所有依赖:
[project]
name = "langchainproject"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"dotenv>=0.9.9",
"gradio>=6.8.0", # Web 界面框架
"langchain>=1.2.10",
"langchain-community>=0.4.1",
"langchain-openai>=1.1.10",
"openai>=2.24.0",
"zhipuai>=2.1.5.20250825", # 智谱 AI SDK(用于 ASR 语音识别)
]
1.3 模型准备
本文涉及两类模型,均已在第一篇文章的 models.py 中配置:
from models import llm # 文本对话模型(如本地 qwen3)
from models import get_multimodal_llm # 多模态模型(如 qwen-omni-3b)
二、对话记忆基础——RunnableWithMessageHistory
多轮对话的核心问题在于:大模型本身是无状态的,每次调用都是一次独立的请求。要让模型"记住"之前的对话内容,必须在每次请求时将历史消息一并传入。
LangChain 的 RunnableWithMessageHistory 封装了这一逻辑——它会在调用链路前自动读取历史消息,调用后自动保存新消息,开发者无需手动管理。
2.1 内存存储:InMemoryChatMessageHistory
最简单的方案是将历史消息 保存在进程内存 中。
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from models import llm
# Step 1:定义提示词模板
prompt = ChatPromptTemplate.from_messages([
('system', '你是一个乐于助人的助手。尽你所能回答所有问题。'),
MessagesPlaceholder(variable_name='chat_history', optional=True),
('human', '{input}')
])
chain = prompt | llm
# Step 2:基于内存的会话存储
store = {} # key: session_id, value: InMemoryChatMessageHistory
def get_session_history(session_id: str):
"""根据会话 ID 获取对应的历史记录"""
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# Step 3:包装为带记忆的链路
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key='input', # 输入字典中,哪个 key 是当前用户消息
history_messages_key='chat_history', # 模板中,哪个占位符接收历史消息
)
# Step 4:多轮对话测试
config = {"configurable": {"session_id": "user_001"}}
res1 = chain_with_history.invoke({'input': '你好,我的工号是 A1024'}, config=config)
print(res1.content)
res2 = chain_with_history.invoke({'input': '我的工号是多少?'}, config=config)
print(res2.content) # 模型能回忆起工号 A1024
关键参数说明:
| 参数 | 作用 |
|---|---|
input_messages_key | 指定输入字典中当前用户消息的 key |
history_messages_key | 指定提示词模板中接收历史消息的占位符名称 |
session_id | 会话隔离标识,不同 session_id 的历史互不干扰 |
局限性:InMemoryChatMessageHistory 存储在进程内存中,程序重启后历史记录全部丢失,仅适用于开发调试阶段。
三、持久化存储——SQLChatMessageHistory
生产环境中,聊天记录需要持久化到数据库。LangChain 内置了 SQLChatMessageHistory,支持将消息存储到关系型数据库(SQLite、PostgreSQL、MySQL 等)。
3.1 从内存切换到 SQLite
只需替换 get_session_history 函数的实现,链路的其他部分完全不变:
from langchain_community.chat_message_histories import SQLChatMessageHistory
def get_session_history(session_id: str):
"""从 SQLite 数据库中获取会话历史"""
return SQLChatMessageHistory(
session_id=session_id,
connection_string='sqlite:///chat_history.db',
)
替换后,消息会自动存入本地 chat_history.db 文件,程序重启后对话记录依然存在。
对比变化:
| 对比项 | InMemoryChatMessageHistory | SQLChatMessageHistory |
|---|---|---|
| 数据存储 | 进程内存(dict) | 关系型数据库文件 |
| 持久性 | 程序重启即丢失 | 永久保存 |
| 多进程共享 | 不支持 | 支持(通过数据库) |
| 手动管理 | 需自行维护 store 字典 | 自动管理,传入 session_id 即可 |
connection_string遵循 SQLAlchemy 格式。SQLite 使用sqlite:///前缀 + 文件路径;若使用 PostgreSQL,则为postgresql://user:pass@host:port/dbname。
四、历史摘要——上下文窗口管理策略
随着对话轮次增加,历史消息会不断累积。大模型的上下文窗口是有限的,过长的历史不仅导致 Token 超限报错,还会增加推理延迟和成本。因此需要一种策略来压缩历史消息。
核心思路:保留最近的 N 条原始消息,将更早的消息交由 LLM 生成摘要。
4.1 策略一:重建历史(直接替换)
该方案在每次调用前检查历史长度,若超过阈值,则清空数据库中的旧记录,用"摘要 + 最近消息"重建历史。
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from models import llm
def summarize_messages(current_input):
"""检查并压缩过长的对话历史"""
session_id = current_input['config']["configurable"]["session_id"]
chat_history = get_session_history(session_id)
stored_messages = chat_history.messages
if len(stored_messages) <= 2:
return False # 消息不多,无需摘要
# 分割:保留最近 2 条,其余用于生成摘要
last_two = stored_messages[-2:]
to_summarize = stored_messages[:-2]
# 构建摘要链
summarization_prompt = ChatPromptTemplate.from_messages([
("system", "请将以下对话历史压缩为一条保留关键信息的摘要消息。"),
("placeholder", "{chat_history}"),
("human", "请生成包含上述对话核心内容的摘要,保留重要事实和决策。")
])
summary_chain = summarization_prompt | llm
summary_message = summary_chain.invoke({'chat_history': to_summarize})
# 重建历史:清空 → 写入摘要 → 写入最近消息
chat_history.clear()
chat_history.add_message(summary_message)
for msg in last_two:
chat_history.add_message(msg)
return True
# 将摘要逻辑注入链路
final_chain = (
RunnablePassthrough.assign(messages_summarized=summarize_messages)
| chain_with_history
)
调用方式:
注意 config 需要同时出现在输入字典和 invoke 的 config 参数中——前者供 summarize_messages 读取 session_id,后者供 RunnableWithMessageHistory 使用。
result = final_chain.invoke(
{'input': '你好', 'config': {"configurable": {"session_id": "user_001"}}},
config={"configurable": {"session_id": "user_001"}}
)
数据流分析:
用户输入 {'input': '...', 'config': {...}}
↓
RunnablePassthrough.assign(messages_summarized=summarize_messages)
├── 透传原始输入
└── 执行 summarize_messages → 检查/压缩历史 → 返回 True/False
↓
合并后 {'input': '...', 'config': {...}, 'messages_summarized': True}
↓
chain_with_history → 读取已压缩的历史 → 调用 LLM → 保存新消息
局限性:chat_history.clear() 是一个破坏性操作,会永久删除原始历史记录。如果摘要生成失败或质量不佳,无法回溯到原始对话。
4.2 策略二:动态系统消息注入(非破坏性)
为解决策略一的破坏性问题,改为将摘要注入到系统消息中,不修改数据库中的原始记录。
核心改动:
- 提示词模板的
system消息改为变量{system_message},支持动态注入摘要内容。 summarize_messages返回结构化字典(而非直接操作数据库)。- 使用两阶段
RunnablePassthrough.assign分步处理数据。
# 提示词模板:system 改为动态变量
prompt = ChatPromptTemplate.from_messages([
('system', "{system_message}"),
MessagesPlaceholder(variable_name='chat_history', optional=True),
('human', '{input}')
])
chain = prompt | llm
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key='input',
history_messages_key='chat_history',
)
def summarize_messages(current_input):
"""非破坏性摘要:返回结构化结果,不修改原始历史"""
session_id = current_input['config']["configurable"]["session_id"]
chat_history = get_session_history(session_id)
stored_messages = chat_history.messages
if len(stored_messages) <= 2:
return {"original_messages": stored_messages, "summary": None}
last_two = stored_messages[-2:]
to_summarize = stored_messages[:-2]
summarization_prompt = ChatPromptTemplate.from_messages([
("system", "请将以下对话历史压缩为一条保留关键信息的摘要消息。"),
("placeholder", "{chat_history}"),
("human", "请生成包含上述对话核心内容的摘要,保留重要事实和决策。")
])
summary_message = (summarization_prompt | llm).invoke({'chat_history': to_summarize})
return {
"original_messages": last_two, # 保留的原始消息
"summary": summary_message # 生成的摘要(AIMessage)
}
# 两阶段 RunnablePassthrough.assign 管线
final_chain = (
RunnablePassthrough.assign(messages_summarized=summarize_messages)
| RunnablePassthrough.assign(
input=lambda x: x['input'],
chat_history=lambda x: x['messages_summarized']['original_messages'],
system_message=lambda x: (
f"你是一个乐于助人的助手。尽你所能回答所有问题。"
f"摘要:{x['messages_summarized']['summary'].content}"
if x['messages_summarized'].get("summary")
else "你是一个乐于助人的助手。尽你所能回答所有问题。"
)
)
| chain_with_history
)
数据流分析(两阶段 assign):
输入: {'input': '你好', 'config': {...}}
↓
第一阶段 assign(messages_summarized=...)
↓
{
'input': '你好',
'config': {...},
'messages_summarized': {
"original_messages": [msg1, msg2],
"summary": AIMessage("用户之前讨论了...")
}
}
↓
第二阶段 assign(input=..., chat_history=..., system_message=...)
↓
{
'input': '你好',
'chat_history': [msg1, msg2], ← 仅最近 2 条
'system_message': '...摘要:用户之前讨论了...',
...
}
↓
chain_with_history → 注入历史 + 系统消息 → 调用 LLM
两种策略对比:
| 对比项 | 策略一(重建历史) | 策略二(动态系统消息) |
|---|---|---|
| 是否修改原始历史 | 是(clear + 重写) | 否(只读) |
| 摘要位置 | 作为历史消息存入数据库 | 注入到 system 消息中 |
| 可回溯性 | 不可逆 | 原始记录完整保留 |
| 实现复杂度 | 较低 | 稍高(两阶段 assign) |
五、构建 Web 界面——Gradio 集成
到目前为止,所有交互都在命令行中完成——用户需要手动编写 invoke 调用、拼接 config 参数,每次对话都要重新运行脚本。这种方式对开发调试尚可,但无法作为产品交付给普通用户。我们需要一个可视化的 Web 界面,让用户通过浏览器即可直接对话。
5.1 界面布局
import gradio as gr
with gr.Blocks(title='聊天机器人', theme=gr.themes.Soft()) as demo:
# 聊天记录展示区
chatbot = gr.Chatbot(height=500, label='聊天机器人')
with gr.Row():
# 左侧:文字输入 + 发送按钮
with gr.Column(scale=4):
user_input = gr.Textbox(
placeholder='请输入消息...',
label='文字输入',
max_lines=5
)
submit_btn = gr.Button('发送', variant="primary")
# 右侧:麦克风录音区
with gr.Column(scale=1):
audio_input = gr.Audio(
sources=['microphone'],
label='语音输入',
type='filepath',
format='wav'
)
界面由三部分组成:顶部是聊天记录展示区(gr.Chatbot),底部左侧是文本输入框和发送按钮,右侧是麦克风录音区域。
5.2 事件绑定
Gradio 的事件机制采用链式调用:一个事件完成后可以通过 .then() 触发下一个事件,形成处理管线。
def add_message(chat_history, user_message):
"""将用户消息追加到聊天记录"""
if user_message:
chat_history.append({"role": "user", "content": user_message})
return chat_history, gr.Textbox(value=None, interactive=False)
def execute_chain(chat_history):
"""调用 LLM 生成回复"""
user_msg = chat_history[-1]
result = final_chain.invoke(
{'input': user_msg['content'], 'config': {"configurable": {"session_id": "user_001"}}},
config={"configurable": {"session_id": "user_001"}}
)
chat_history.append({'role': 'assistant', 'content': result.content})
return chat_history
# 文本提交事件链:追加消息 → 调用 LLM 生成回复
chat_msg = user_input.submit(add_message, [chatbot, user_input], [chatbot, user_input])
chat_msg.then(execute_chain, chatbot, chatbot)
# 按钮点击事件(逻辑相同)
submit_btn.click(
add_message, [chatbot, user_input], [chatbot, user_input]
).then(execute_chain, chatbot, chatbot)
事件流:
用户按下回车 / 点击发送
↓
add_message() → 追加 user 消息到 chatbot,锁定输入框(防止重复提交)
↓ .then()
execute_chain() → 取最后一条 user 消息 → 调用 LLM → 追加 assistant 回复
↓
界面自动刷新,显示新消息
启动应用:
if __name__ == '__main__':
demo.launch()
add_message返回gr.Textbox(value=None, interactive=False)的作用是清空输入框并暂时禁用交互,避免用户在模型响应期间重复提交。待回复完成后,可通过.then()重新激活输入框。
六、语音输入——ASR 语音识别集成
目前的聊天界面只支持键盘输入,但在移动端或免提场景下,打字并不方便。为了提升交互体验,我们为界面添加语音输入功能——通过 ASR(Automatic Speech Recognition)服务将录音转为文字,再走已有的文本对话链路。
6.1 接入智谱 ASR 服务
from zhipuai import ZhipuAI
from env_utils import get_env
def read_audio(audio_path):
"""调用智谱 ASR 将音频转为文字"""
if not audio_path:
return ''
client = ZhipuAI(api_key=get_env("ZHIPU_API_KEY"))
with open(audio_path, "rb") as audio_file:
resp = client.audio.transcriptions.create(
model="glm-asr",
file=audio_file,
stream=False
)
return resp.model_extra['text']
6.2 绑定语音事件
# 录音完成后,自动将语音转为文字并填入输入框
audio_input.change(read_audio, [audio_input], [user_input])
交互流程:
用户通过麦克风录音
↓ 录音结束,触发 change 事件
read_audio() → 调用 ZhipuAI ASR → 返回识别文字
↓
文字自动填入输入框
↓ 用户点击发送
走正常的文本对话流程(add_message → execute_chain)
该方案的本质是语音转文字,最终仍以文本形式与 LLM 交互,因此不需要多模态模型。适合不具备多模态模型但需要语音输入的场景。
七、全模态融合——文本 + 音频 + 图像
前面的方案将语音先转为文字再交给文本模型处理,这属于间接方案。如果使用多模态大模型(如 qwen-omni),则可以让模型直接理解音频和图像内容,无需中间转换。
7.1 架构变化
| 对比项 | 第六章(ASR 方案) | 第七章(原生多模态) |
|---|---|---|
| 模型类型 | 文本模型 | 多模态模型 |
| 音频处理 | ASR 转文字 → 文本输入 | Base64 编码 → 直接传入模型 |
| 图像处理 | 不支持 | Base64 编码 → 直接传入模型 |
| 消息格式 | 'input': '纯文本' | HumanMessage(content=[多类型列表]) |
核心变化:HumanMessage 的 content 字段从简单字符串变为混合类型的内容列表,可同时包含文本、音频和图像。
7.2 多模态提示词模板
由于多模态输入需要构造 HumanMessage 对象(包含 content 列表),无法使用简单的 ('human', '{input}') 占位符。改为使用 MessagesPlaceholder 直接注入消息对象:
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from models import get_multimodal_llm
multimodal_llm = get_multimodal_llm()
prompt = ChatPromptTemplate.from_messages([
('system', "你是一个多模态 AI 助手,可以处理文本、音频和图像输入"),
MessagesPlaceholder(variable_name="messages"),
])
chain = prompt | multimodal_llm
# 多模态链路的历史管理
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
)
注意此处
RunnableWithMessageHistory未指定input_messages_key和history_messages_key,因为输入直接以HumanMessage对象的形式传入messages字段,历史消息会自动注入到该占位符中。
7.3 音频处理:Base64 编码
将本地音频文件编码为 Base64 字符串,封装为模型可识别的 audio_url 消息格式。
import base64
def transcribe_audio(audio_path):
"""将音频文件编码为 Base64 并封装为消息格式"""
try:
with open(audio_path, 'rb') as f:
audio_data = base64.b64encode(f.read()).decode('utf-8')
return {
"type": "audio_url",
"audio_url": {
"url": f"data:audio/wav;base64,{audio_data}",
"duration": 30 # 单位:秒,帮助模型优化处理
}
}
except Exception as e:
print(e)
return {}
多模态模型通常支持两种传参方式:Base64 编码(适合本地文件)和网络 URL(适合已部署到服务器的资源)。本文使用 Base64 方式。
7.4 图像处理:PIL + Base64
使用 Pillow 读取图片并保留原始格式,避免格式转换导致信息丢失(如 PNG 的透明通道)。
import io
from PIL import Image
def transcribe_image(image_path):
"""将图片转为 Base64 编码的 data URL"""
with Image.open(image_path) as img:
img_format = img.format if img.format else 'JPEG'
buffered = io.BytesIO()
img.save(buffered, format=img_format)
image_data = base64.b64encode(buffered.getvalue()).decode('utf-8')
return {
"type": "image_url",
"image_url": {
"url": f"data:image/{img_format.lower()};base64,{image_data}",
"detail": "low"
}
}
detail参数控制图像分辨率:low为低分辨率(节省 Token),high为高分辨率(细节更丰富)。可根据业务需求调整。
7.5 统一输入组件:MultimodalTextbox
前面第五章的界面采用了文本框 + 麦克风分离的布局,存在两个问题:
- 不支持图片上传:界面中没有图片输入入口,无法发挥多模态模型的图像理解能力。
- 交互割裂:用户需要先录音等待转写,再手动点击发送,无法在一次提交中同时发送文本、图片和音频。
为此,我们将输入组件替换为 Gradio 的 MultimodalTextbox,它允许用户在同一个输入框中同时发送文本和文件:
chat_input = gr.MultimodalTextbox(
interactive=True,
file_types=['image', '.wav', '.mp4'], # 支持的文件类型
file_count="multiple", # 允许多文件上传
placeholder="请输入信息或上传文件...",
show_label=False,
sources=["microphone", "upload"], # 支持麦克风和文件上传
)
该组件提交的数据结构为:
{'text': '用户输入的文字', 'files': ['/path/to/file1.jpg', '/path/to/file2.wav']}
7.6 消息解析与分发逻辑
需要将 MultimodalTextbox 提交的数据解析为 HumanMessage(content=[...]) 格式,根据文件扩展名调用对应的编码函数。
def get_last_user_after_assistant(history):
"""从聊天记录中提取最后一轮 assistant 回复之后的所有 user 消息"""
if not history or history[-1]["role"] == "assistant":
return None
last_assistant_idx = -1
for i in range(len(history) - 1, -1, -1):
if history[i]["role"] == "assistant":
last_assistant_idx = i
break
return history if last_assistant_idx == -1 else history[last_assistant_idx + 1:]
def add_message(history, messages):
"""将用户输入(文件 + 文字)追加到聊天记录"""
for f in messages['files']:
history.append({'role': 'user', 'content': {'path': f}})
if messages['text']:
history.append({'role': 'user', 'content': messages['text']})
return history, gr.MultimodalTextbox(value=None, interactive=False)
def submit_messages(history):
"""解析用户消息,构造 HumanMessage 并调用多模态链路"""
user_messages = get_last_user_after_assistant(history)
content = []
if user_messages:
for msg in user_messages:
if isinstance(msg['content'], str):
# 文本消息
content.append({'type': 'text', 'text': msg['content']})
elif isinstance(msg['content'], tuple):
# 文件消息(Gradio 以 tuple 形式传递文件路径)
file_path = msg['content'][0]
if file_path.endswith('.wav'):
content.append(transcribe_audio(file_path))
elif file_path.endswith(('.jpg', '.png', '.jpeg')):
content.append(transcribe_image(file_path))
input_message = HumanMessage(content=content)
resp = chain_with_history.invoke({'messages': input_message}, config)
history.append({'role': 'assistant', 'content': resp.content})
return history
消息构造示意:
用户发送:一段文字 + 一张图片 + 一段录音
↓ 解析为
HumanMessage(content=[
{'type': 'text', 'text': '请描述这张图片中的内容'},
{'type': 'image_url', 'image_url': {'url': 'data:image/jpeg;base64,...'}},
{'type': 'audio_url', 'audio_url': {'url': 'data:audio/wav;base64,...'}}
])
↓ 传入多模态 LLM
模型同时理解文字、图像和音频内容 → 生成综合回复
get_last_user_after_assistant的作用是:当用户连续发送多条消息(先传图片、再发文字)后才触发回复时,需要将这些消息合并为一个HumanMessage,确保模型能完整理解用户意图。
7.7 完整界面代码
import uuid
import gradio as gr
config = {"configurable": {"session_id": str(uuid.uuid4())}}
with gr.Blocks(title='多模态聊天机器人', theme=gr.themes.Soft()) as demo:
chatbot = gr.Chatbot(height=500, label='聊天机器人')
chat_input = gr.MultimodalTextbox(
interactive=True,
file_types=['image', '.wav', '.mp4'],
file_count="multiple",
placeholder="请输入信息或上传文件...",
show_label=False,
sources=["microphone", "upload"],
)
chat_input.submit(
add_message, [chatbot, chat_input], [chatbot, chat_input]
).then(
submit_messages, [chatbot], [chatbot]
).then(
lambda: gr.MultimodalTextbox(interactive=True), # 回复完成后重新激活输入框
None,
[chat_input]
)
if __name__ == '__main__':
demo.launch()
事件链:
用户提交(文字 + 文件)
↓
add_message() → 将文件和文字分别追加到 chatbot 记录,锁定输入框
↓ .then()
submit_messages() → 解析消息类型 → Base64 编码 → 构造 HumanMessage → 调用多模态 LLM → 追加回复
↓ .then()
lambda → 重新激活输入框,允许下一轮输入
八、总结:核心组件速查表
| 组件 | 核心能力 | 本文应用场景 |
|---|---|---|
| RunnableWithMessageHistory | 自动管理对话历史的读写 | 所有章节的对话链路核心 |
| InMemoryChatMessageHistory | 基于内存的历史存储 | 开发调试阶段的快速验证 |
| SQLChatMessageHistory | 基于数据库的持久化存储 | 生产环境的对话记录持久化 |
| RunnablePassthrough.assign | 透传数据并追加计算字段 | 注入摘要逻辑到链路中 |
| HumanMessage(content=[…]) | 混合类型内容列表 | 多模态输入(文本 + 图片 + 音频) |
| gr.Blocks | 构建 Web 交互界面 | 聊天机器人的可视化前端 |
| gr.MultimodalTextbox | 支持文字 + 文件的统一输入 | 多模态聊天的用户输入组件 |
| Base64 编码 | 将二进制文件转为文本格式传输 | 图片和音频的本地文件传参方式 |
九、完整代码
写在最后
本文从最基础的内存对话记忆出发,逐步引入持久化存储、历史摘要、Web 界面、语音识别,最终实现了一个支持文本、音频、图像的多模态聊天机器人。在这个过程中,前两篇文章介绍的提示词模板、LCEL 链式编排、RunnablePassthrough 数据透传等核心概念不再是孤立的知识点,而是被串联成了一条从后端链路到前端交互的完整链路。
如有疑问或建议,欢迎留言讨论!
更多推荐



所有评论(0)