Streamlit 实现 AI 聊天 Agent:角色提示词、流式输出与会话管理

这个 Demo 用 Streamlit 搭建了一个可配置角色的 AI 聊天页面:用户输入消息后,程序把角色提示词和历史消息发给模型,并将回复显示在聊天区域。聊天状态保存在 st.session_state,历史会话则写入本地 JSON 文件,用户可以在侧边栏新建、加载和删除会话。

1. 页面状态:聊天为什么能跨脚本重运行保留

Streamlit 的交互会触发脚本重新运行。程序用 st.session_state 保存聊天内容和当前设置,让页面重运行后仍能重绘聊天记录。

python 复制代码
# ====== 初始化 session_state(必须在使用前初始化) ======
if "messages" not in st.session_state:
    st.session_state["messages"] = []
if "nick_name" not in st.session_state:
    st.session_state["nick_name"] = "真名波"
if "nature" not in st.session_state:
    st.session_state["nature"] = "活泼开朗的东北姑娘"
# 空白对话页不预先创建会话,首次发送消息时再生成标识
if "current_session" not in st.session_state:
    st.session_state["current_session"] = None

这几个状态字段各自负责一类数据:

  • messages 是当前聊天记录,元素形如 {"role": "user", "content": "你好"}。
  • nick_name 是 Agent 在提示词中使用的昵称。
  • nature 是 Agent 的性格描述。
  • current_session 是当前会话文件对应的标识;为 None 时表示还没有建立会话。

在页面主体中,程序遍历 messages 并重新显示历史聊天:

python 复制代码
#显示聊天记录
for message in st.session_state["messages"]:
    st.chat_message(message["role"]).write(message["content"])

st.chat_message 根据消息的 role 创建聊天消息框。user 和 assistant 分别表示用户和助手,因此同一份消息列表既是传给模型的上下文,也是页面恢复聊天记录的数据来源。

这里的 session_state 只负责当前 Streamlit 会话里的内存状态。浏览器刷新、应用重启后要恢复历史记录,需要从后面介绍的 JSON 文件重新加载。

2. 系统提示词:用昵称和性格约束 Agent 的回复

这个 Demo 通过系统提示词设定 Agent 的角色、回复风格和行为规则:

python 复制代码
# 系统提示词(此时 nick_name 和 nature 已经初始化好,可以安全使用)
system_prompt = f"""
#你叫{st.session_state["nick_name"]},现在是用户的真实伴侣,请完全代入伴侣角色。

规则:

    1. 每次只回1条消息
    2. 禁止任何场景或状态描述性文字
    3. 匹配用户的语言
    4. 回复简短,像微信聊天一样
    5. 有需要的话可以用❤️🌸等emoji表情
    6. 用符合伴侣性格的方式对话
    7. 回复的内容,要充分体现伴侣的性格特征

伴侣性格:

    - {st.session_state["nature"]}

"""

nick_name 和 nature 通过 Python 的 f-string 插入提示词。用户在侧边栏修改这两个值后,后续页面运行会根据新状态重新生成 system_prompt。

系统提示词只是一段发给模型的文本指令,不会在本地代码中强制执行规则。模型是否遵守"回复简短"或"匹配用户语言"等要求,仍取决于模型的响应。提示词的作用是明确角色和预期行为,不等于程序层面的校验。

客户端配置将 DEEPSEEK_API_KEY 作为密钥来源,并指定请求服务地址:

python 复制代码
client = OpenAI(
    api_key=os.environ.get('DEEPSEEK_API_KEY'),
    base_url="https://api.deepseek.com",
)

api_key 从进程环境变量读取,避免把密钥直接写进脚本;base_url 指向 DeepSeek 服务。运行应用前,需要在启动 Streamlit 的终端中设置 DEEPSEEK_API_KEY。如果密钥缺失或无效,请求会失败;当前脚本没有对模型请求单独添加异常处理。

3. 聊天请求:系统提示词与历史消息如何组合

用户提交消息后,代码先确保当前会话有标识,再把用户消息追加到聊天状态。随后将系统提示词和完整的聊天历史一起传给模型:

python 复制代码
#输入框
prompt=st.chat_input("请输入:")
if prompt:#字符串会自动转换为布尔值
    # 删除会话后保持空白页,只有用户真正发消息时才创建新的会话
    if not st.session_state["current_session"]:
        st.session_state["current_session"] = generate_session_name()

    st.chat_message("user").write(prompt)
    #将用户消息添加到会话状态
    st.session_state["messages"].append({"role": "user", "content": prompt})

    #调用大模型
    response = client.chat.completions.create(
        model="deepseek-flash",
        messages=[
            {"role": "system", "content": system_prompt},
            *st.session_state["messages"],
        ],
        stream=True,
    )

st.chat_input 在用户提交内容时返回文本。新会话第一次收到消息时,代码才生成会话标识;随后把用户消息追加到状态,并连同系统提示词一起传给模型。传入模型的消息顺序是系统消息在前,当前完整聊天历史在后。每次请求都会携带目前保存的整段对话;随着聊天轮数增长,请求上下文也会变长,脚本没有做历史裁剪或摘要。

4. 非流式输出与流式输出

非流式写法:等回复生成完成后再显示

脚本中保留了一行注释,展示了非流式读取结果的写法:

python 复制代码
# 显示大模型的回复(非流式)
# st.chat_message("assistant").write(response.choices[0].message.content)

非流式请求通常把 stream 设置为 False 或省略该参数。模型完成整条回复后,程序再从响应对象中读取完整文本并显示:

python 复制代码
response = client.chat.completions.create(
    model="deepseek-flash",
    messages=[
        {"role": "system", "content": system_prompt},
        *st.session_state["messages"],
    ],
    stream=False,
)

full_response = response.choices[0].message.content
st.chat_message("assistant").write(full_response)

response.choices0.message.content 是完整回复文本。非流式写法更直接,适合一次性拿到结果的场景;代价是用户需要等模型生成完成后才能看到回复。

流式写法:收到文本片段后逐步更新页面

当前脚本实际使用的是流式输出。接口每次返回一个数据块,程序从 delta.content 取出该块的文本,累积到 full_response,并更新占位区域:

python 复制代码
    # 显示大模型的回复(非流式)
    # st.chat_message("assistant").write(response.choices[0].message.content)
    # 显示大模型的回复(流式)
    response_message=st.empty() #创建一个空的容器,用于存储大模型的回复
    full_response=""
    for chunk in response:
        if chunk.choices[0].delta.content is not None:
            content=chunk.choices[0].delta.content
            full_response+=content
            response_message.chat_message("assistant").write(full_response)
    #将大模型回复添加到会话状态
    st.session_state["messages"].append({"role": "assistant", "content": full_response})

content 是当前数据块中的文本片段。部分数据块可能没有文本,所以代码先判断它是否为 None。full_response 保存到目前为止收到的全部文本;每次收到新片段,程序都将它拼接到字符串末尾。

st.empty() 创建一个可被后续内容替换的占位区域。循环中每次写入的都是当前累计文本,因此页面会逐步显示越来越长的回复。循环结束后,完整回复被追加到 messages,成为后续请求的聊天上下文。

两种方式的核心区别是:**非流式等完整结果返回后一次显示;流式则边接收文本片段边更新界面。**本脚本选择流式方式,非流式代码只作为对照,并未参与当前运行。

5. 会话文件:什么时候保存,保存了什么

会话标识由当前时间生成:

python 复制代码
#生成会话标识
def generate_session_name():
    return datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

标识使用年月日、时、分、秒格式,同时也是 JSON 文件名的一部分,例如 sessions/2026-10-27_14-30-05.json。

保存函数会把昵称、性格、当前会话标识和完整消息列表写入文件:

python 复制代码
#保存会话信息
def save_session():
    if st.session_state.current_session:
        # 构建新的会话对象
        session_data = {
            "nick_name": st.session_state["nick_name"],
            "nature": st.session_state["nature"],
            "current_session": st.session_state["current_session"],
            "messages": st.session_state["messages"],
        }
        # 如果sessions目录不存在,创建一个
        if not os.path.exists("sessions"):
            os.mkdir("sessions")
        # 保存会话数据到文件
        with open(f"sessions/{st.session_state['current_session']}.json", "w", encoding="utf-8") as f:
            json.dump(session_data, f, ensure_ascii=False, indent=4)

保存动作是覆盖写入:同一个会话标识对应同一个文件,每次保存都会用当前状态重写该文件。ensure_ascii=False 让文件保留中文字符;indent=4 让 JSON 带缩进,方便检查内容。

脚本在以下几个位置调用 save_session():

  • 模型回复完成后:助手回复已加入 messages,随后保存整段会话。
  • 点击"新建会话"时:先保存当前已有会话,再清空消息并重置会话标识。
  • 修改昵称或性格时:侧边栏输入框有非空值时,更新状态并调用保存函数。

最后一种情况只有在 current_session 已经有值时才会真正写文件,因为 save_session() 开头做了判断。刚打开应用或刚点击新建会话后,如果还没有发送消息,就不会产生空白会话文件。

6. 侧边栏操作:新建、加载与删除

新建会话

点击新建按钮时,程序先保存当前会话,再清空聊天记录并重置当前标识:

python 复制代码
if st.button("新建会话",width="stretch"):
    # 只保存已有消息的会话,不为新的空白页创建会话文件
    save_session()
    st.session_state["messages"] = []
    st.session_state["current_session"] = None
    st.rerun()  # 刷新页面,进入空白对话页

st.rerun() 会让脚本立即重新运行。因为 current_session 被重置为 None,页面进入空白聊天状态;等用户再次发消息时才会创建新的会话标识。

列出并加载历史会话

程序扫描 sessions 目录,将 JSON 文件名作为会话列表显示:

python 复制代码
#列出所有会话文件
def list_sessions():
   session_list=[]
   if os.path.exists("sessions"):
       file_list =os.listdir("sessions")
       for filename in file_list:
           if filename.endswith(".json"):
               session_list.append(filename[:-5])
               session_list.sort(reverse=True)
   return session_list

文件名去掉 .json 后成为列表项。年月日时分秒格式的标识按字符串倒序排列时,较新的会话会排在前面。

用户点击某个会话后,程序打开对应文件并恢复保存的状态:

python 复制代码
#加载指定的会话信息
def load_session(session_name):
    try:
        if os.path.exists(f"sessions/{session_name}.json"):
        # 加载会话数据
            with open(f"sessions/{session_name}.json", "r", encoding="utf-8") as f:
                session_data = json.load(f)
                st.session_state["nick_name"] = session_data["nick_name"]
                st.session_state["nature"] = session_data["nature"]
                st.session_state["current_session"] = session_data["current_session"]
                st.session_state["messages"] = session_data["messages"]
                st.rerun() #重新运行页面,刷新会话状态
    except Exception as e:
        st.error(f"加载会话时出错: {e}")

json.load 解析文件;成功后,昵称、性格、会话标识和消息列表都会恢复到 session_state。随后脚本重新运行并绘制页面,历史消息就会显示出来。加载或解析文件出错时,程序通过 st.error 在页面显示错误信息。

删除会话

点击会话旁的删除按钮后,程序删除对应文件。如果删的是当前会话,还会清空当前页面中的聊天状态:

python 复制代码
# 删除指定的会话文件
def delete_session(session_name):
    try:
        if os.path.exists(f"sessions/{session_name}.json"):
            os.remove(f"sessions/{session_name}.json")
            # 删除当前会话后进入空白对话页,等用户发消息时再创建会话
        if session_name == st.session_state["current_session"]:
            st.session_state["messages"] = []
            st.session_state["current_session"] = None
    except Exception as e:
        st.error(f"删除会话时出错: {e}")

侧边栏的删除按钮在调用该函数后还会执行 st.rerun(),所以会话列表会刷新。若删除的是其他会话,当前聊天内容仍保留。

总结

这个 Agent Demo 用系统提示词描述角色,用聊天历史提供上下文,用流式响应逐步显示模型输出;Streamlit 状态维护当前对话,JSON 文件保存可加载的历史会话。理解会话操作时,重点看按钮触发后状态如何变化,以及 save_session() 在哪些位置被调用。

相关推荐
niucloud-admin1 小时前
NIUCLOUD PHP SAAS上门家政使用文档:客户报表
前端·人工智能·ai编程
小南知更鸟1 小时前
【2026 年 Python 包管理工具选型:从“工具崇拜”回到工程约束】
开发语言·python
漫步人生走在路上1 小时前
外贸B2B行业GEO优化白皮书:从0到1完整落地流程
前端·人工智能·搜索引擎·chatgpt·twitter
TomEval1 小时前
【测AI】第20篇:大模型安全测试 —— 越狱攻击与防护
人工智能·python·功能测试·ai·aigc·测试用例·安全性测试
霖霖7141 小时前
【笔记】直流减速电机(GA25-370)调速
笔记·单片机·mcu
RuoyiOffice1 小时前
达梦 DM8 + openGauss:RuoYi Office 的 MySQL 数据转换与导入实践
数据库·spring boot·python·sql·信创
用户0332126663671 小时前
轻松使用 Python 将 RTF 转换为 Word 文档(含批量转换与格式互转)
python
努力努力再努力wz1 小时前
【Lua入门系列】从动态类型到变量作用域:彻底理解 local、全局变量与跨文件访问
开发语言·junit·lua