Python 调用 AI 接口,用 Streamlit 做一个能保存会话的智能伴侣

学完 Python 基础以后,很多人最想做的项目就是 AI 应用。

这件事其实没有想象中神秘。

最小闭环就是:

用户输入问题,Python 程序把问题发给模型 API,模型返回回答,程序把回答展示出来。

如果再往前走一步,就把对话历史保存下来,做成一个简单的 AI 智能伴侣。

AI 应用的数据流

JSON 会话文件 模型 API Python 逻辑 Streamlit 页面 用户 JSON 会话文件 模型 API Python 逻辑 Streamlit 页面 用户 #mermaid-svg-OXQWPIxLVonBdIwW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OXQWPIxLVonBdIwW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OXQWPIxLVonBdIwW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OXQWPIxLVonBdIwW .error-icon{fill:#552222;}#mermaid-svg-OXQWPIxLVonBdIwW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OXQWPIxLVonBdIwW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OXQWPIxLVonBdIwW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OXQWPIxLVonBdIwW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OXQWPIxLVonBdIwW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OXQWPIxLVonBdIwW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OXQWPIxLVonBdIwW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OXQWPIxLVonBdIwW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OXQWPIxLVonBdIwW .marker.cross{stroke:#333333;}#mermaid-svg-OXQWPIxLVonBdIwW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OXQWPIxLVonBdIwW p{margin:0;}#mermaid-svg-OXQWPIxLVonBdIwW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OXQWPIxLVonBdIwW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-OXQWPIxLVonBdIwW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OXQWPIxLVonBdIwW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-OXQWPIxLVonBdIwW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-OXQWPIxLVonBdIwW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-OXQWPIxLVonBdIwW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-OXQWPIxLVonBdIwW .sequenceNumber{fill:white;}#mermaid-svg-OXQWPIxLVonBdIwW #sequencenumber{fill:#333;}#mermaid-svg-OXQWPIxLVonBdIwW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-OXQWPIxLVonBdIwW .messageText{fill:#333;stroke:none;}#mermaid-svg-OXQWPIxLVonBdIwW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OXQWPIxLVonBdIwW .labelText,#mermaid-svg-OXQWPIxLVonBdIwW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-OXQWPIxLVonBdIwW .loopText,#mermaid-svg-OXQWPIxLVonBdIwW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-OXQWPIxLVonBdIwW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OXQWPIxLVonBdIwW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-OXQWPIxLVonBdIwW .noteText,#mermaid-svg-OXQWPIxLVonBdIwW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-OXQWPIxLVonBdIwW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OXQWPIxLVonBdIwW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OXQWPIxLVonBdIwW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OXQWPIxLVonBdIwW .actorPopupMenu{position:absolute;}#mermaid-svg-OXQWPIxLVonBdIwW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-OXQWPIxLVonBdIwW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OXQWPIxLVonBdIwW .actor-man circle,#mermaid-svg-OXQWPIxLVonBdIwW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-OXQWPIxLVonBdIwW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入问题 追加 user 消息 发送 messages 返回 assistant 消息 展示回答 保存会话历史

这里最核心的数据结构是 messages

python 复制代码
messages = [
    {"role": "system", "content": "你是一个耐心的 Python 助教。"},
    {"role": "user", "content": "变量是什么?"},
    {"role": "assistant", "content": "变量可以理解成数据的名字。"}
]

system 设定角色和规则。

user 是用户说的话。

assistant 是模型返回的话。

模型不是凭空知道上下文的。你把哪些历史消息传过去,它就基于哪些上下文回答。

准备环境

安装依赖:

bash 复制代码
pip install openai streamlit

课程资料里使用的是 OpenAI 兼容 SDK 调用 DeepSeek:

python 复制代码
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ.get("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com"
)

这里有一个必须强调的点:

API Key 不要写死在代码里。

Windows PowerShell 临时设置:

powershell 复制代码
$env:DEEPSEEK_API_KEY="你的密钥"

Python 里读取:

python 复制代码
import os

api_key = os.environ.get("DEEPSEEK_API_KEY")

if not api_key:
    raise RuntimeError("请先配置 DEEPSEEK_API_KEY 环境变量")

这样代码可以分享,密钥不跟着泄露。

最小 AI 调用

python 复制代码
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ.get("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com"
)

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "你是一个耐心的 Python 助教。"},
        {"role": "user", "content": "用一句话解释变量是什么。"}
    ],
    stream=False
)

print(response.choices[0].message.content)

这段代码的重点:

client 是 API 客户端。

model 是模型名。

messages 是对话上下文。

response.choices[0].message.content 是返回文本。

system 提示词怎么写

不要只写:

python 复制代码
{"role": "system", "content": "你是一个助手。"}

太空。

更具体一点:

python 复制代码
{
    "role": "system",
    "content": "你是一个耐心的 Python 助教,用初学者能听懂的话解释,每次回答都给一个短代码示例。"
}

提示词不是玄学,它是在告诉模型回答的角色、读者、风格和输出要求。

越具体,越稳定。

Streamlit 页面

Streamlit 适合快速做 Python Web 原型。

创建 ai_partner.py

python 复制代码
import os

import streamlit as st
from openai import OpenAI

st.set_page_config(page_title="AI 智能伴侣", page_icon="AI")
st.title("AI 智能伴侣")

api_key = os.environ.get("DEEPSEEK_API_KEY")

if not api_key:
    st.error("请先配置 DEEPSEEK_API_KEY 环境变量")
    st.stop()

client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com")

if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "你是一个耐心的 Python 学习助手。"}
    ]

for message in st.session_state.messages:
    if message["role"] == "system":
        continue

    with st.chat_message(message["role"]):
        st.write(message["content"])

user_input = st.chat_input("输入你的问题")

if user_input:
    st.session_state.messages.append({"role": "user", "content": user_input})

    with st.chat_message("user"):
        st.write(user_input)

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=st.session_state.messages,
        stream=False
    )

    answer = response.choices[0].message.content
    st.session_state.messages.append({"role": "assistant", "content": answer})

    with st.chat_message("assistant"):
        st.write(answer)

运行:

bash 复制代码
streamlit run ai_partner.py

为什么要用 session_state

Streamlit 有一个很重要的机制:

用户每次交互,脚本会从上到下重新运行。

如果你用普通变量保存消息,每次重新运行都会丢。

st.session_state 可以在同一个浏览器会话里保存状态。

流程是这样:
#mermaid-svg-yZVCPmrZFHYeqAOL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yZVCPmrZFHYeqAOL .error-icon{fill:#552222;}#mermaid-svg-yZVCPmrZFHYeqAOL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yZVCPmrZFHYeqAOL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yZVCPmrZFHYeqAOL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yZVCPmrZFHYeqAOL .marker.cross{stroke:#333333;}#mermaid-svg-yZVCPmrZFHYeqAOL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yZVCPmrZFHYeqAOL p{margin:0;}#mermaid-svg-yZVCPmrZFHYeqAOL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yZVCPmrZFHYeqAOL .cluster-label text{fill:#333;}#mermaid-svg-yZVCPmrZFHYeqAOL .cluster-label span{color:#333;}#mermaid-svg-yZVCPmrZFHYeqAOL .cluster-label span p{background-color:transparent;}#mermaid-svg-yZVCPmrZFHYeqAOL .label text,#mermaid-svg-yZVCPmrZFHYeqAOL span{fill:#333;color:#333;}#mermaid-svg-yZVCPmrZFHYeqAOL .node rect,#mermaid-svg-yZVCPmrZFHYeqAOL .node circle,#mermaid-svg-yZVCPmrZFHYeqAOL .node ellipse,#mermaid-svg-yZVCPmrZFHYeqAOL .node polygon,#mermaid-svg-yZVCPmrZFHYeqAOL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yZVCPmrZFHYeqAOL .rough-node .label text,#mermaid-svg-yZVCPmrZFHYeqAOL .node .label text,#mermaid-svg-yZVCPmrZFHYeqAOL .image-shape .label,#mermaid-svg-yZVCPmrZFHYeqAOL .icon-shape .label{text-anchor:middle;}#mermaid-svg-yZVCPmrZFHYeqAOL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yZVCPmrZFHYeqAOL .rough-node .label,#mermaid-svg-yZVCPmrZFHYeqAOL .node .label,#mermaid-svg-yZVCPmrZFHYeqAOL .image-shape .label,#mermaid-svg-yZVCPmrZFHYeqAOL .icon-shape .label{text-align:center;}#mermaid-svg-yZVCPmrZFHYeqAOL .node.clickable{cursor:pointer;}#mermaid-svg-yZVCPmrZFHYeqAOL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yZVCPmrZFHYeqAOL .arrowheadPath{fill:#333333;}#mermaid-svg-yZVCPmrZFHYeqAOL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yZVCPmrZFHYeqAOL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yZVCPmrZFHYeqAOL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yZVCPmrZFHYeqAOL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yZVCPmrZFHYeqAOL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yZVCPmrZFHYeqAOL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yZVCPmrZFHYeqAOL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yZVCPmrZFHYeqAOL .cluster text{fill:#333;}#mermaid-svg-yZVCPmrZFHYeqAOL .cluster span{color:#333;}#mermaid-svg-yZVCPmrZFHYeqAOL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yZVCPmrZFHYeqAOL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yZVCPmrZFHYeqAOL rect.text{fill:none;stroke-width:0;}#mermaid-svg-yZVCPmrZFHYeqAOL .icon-shape,#mermaid-svg-yZVCPmrZFHYeqAOL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yZVCPmrZFHYeqAOL .icon-shape p,#mermaid-svg-yZVCPmrZFHYeqAOL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yZVCPmrZFHYeqAOL .icon-shape .label rect,#mermaid-svg-yZVCPmrZFHYeqAOL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yZVCPmrZFHYeqAOL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yZVCPmrZFHYeqAOL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yZVCPmrZFHYeqAOL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有

用户输入
Streamlit 重新运行脚本
session_state 里有 messages 吗?
初始化系统提示词
读取历史消息
追加用户消息
调用模型
追加 assistant 回复

这也是很多初学者写 Streamlit 聊天应用时最容易卡住的地方。

不是模型忘了,是你的程序没有保存状态。

保存会话到 JSON

session_state 只在当前浏览器会话里有效。

如果要关闭页面后还能保留历史,需要保存到文件。

python 复制代码
import json
from pathlib import Path

SESSION_FILE = Path("session.json")


def load_messages() -> list[dict]:
    """读取会话历史,文件不存在时返回默认系统消息。"""
    if not SESSION_FILE.exists():
        return [{"role": "system", "content": "你是一个耐心的 Python 学习助手。"}]

    with SESSION_FILE.open("r", encoding="utf-8") as file:
        return json.load(file)


def save_messages(messages: list[dict]) -> None:
    """保存会话历史到 JSON 文件。"""
    with SESSION_FILE.open("w", encoding="utf-8") as file:
        json.dump(messages, file, ensure_ascii=False, indent=2)

页面初始化时:

python 复制代码
if "messages" not in st.session_state:
    st.session_state.messages = load_messages()

每次模型回复后:

python 复制代码
save_messages(st.session_state.messages)

这样就能形成持久会话。

错误处理

API 调用可能失败。

原因可能是密钥错误、网络问题、模型名错误、额度不足。

可以先做简单处理:

python 复制代码
def ask_ai(client: OpenAI, messages: list[dict]) -> str:
    """调用模型并返回文本回答。"""
    try:
        response = client.chat.completions.create(
            model="deepseek-chat",
            messages=messages,
            stream=False
        )
        return response.choices[0].message.content
    except Exception as error:
        return f"模型调用失败:{error}"

真实项目里不建议直接把完整错误暴露给普通用户,但入门阶段这样方便定位问题。

会话历史不能无限长

每次调用模型,都把 messages 发过去。

消息越多,请求越长,响应越慢,成本也可能越高。

入门阶段可以先限制最近若干轮:

python 复制代码
def trim_messages(messages: list[dict], max_items: int = 10) -> list[dict]:
    system_messages = [message for message in messages if message["role"] == "system"]
    chat_messages = [message for message in messages if message["role"] != "system"]

    return system_messages + chat_messages[-max_items:]

这样保留系统提示词,再保留最近对话。

完整项目结构

建议结构:

text 复制代码
ai-partner
├── app.py
├── sessions
│   └── session.json
└── requirements.txt

requirements.txt

text 复制代码
openai
streamlit

安装:

bash 复制代码
pip install -r requirements.txt

运行:

bash 复制代码
streamlit run app.py

常见问题

页面每次输入后历史消失

原因:没有用 st.session_state

修复:把消息列表保存到 st.session_state.messages

API Key 明明设置了还是读不到

原因可能是你设置环境变量的命令行窗口和运行 Streamlit 的窗口不是同一个。

修复:在运行 Streamlit 的同一个终端里设置环境变量。

模型回复风格不稳定

原因:system 提示词太模糊。

修复:明确角色、读者、回答长度、是否给代码示例。

程序越来越慢

原因:历史消息无限增长。

修复:限制最近若干轮,或把早期对话摘要化。

练习

做一个 Python 学习助手:

  1. system 提示词设为 Python 助教。
  2. 页面显示历史消息。
  3. 每次输入后调用模型回答。
  4. 会话保存到 session.json
  5. 增加一个清空会话按钮。

清空按钮可以这样写:

python 复制代码
if st.button("清空会话"):
    st.session_state.messages = [
        {"role": "system", "content": "你是一个耐心的 Python 学习助手。"}
    ]
    save_messages(st.session_state.messages)

参考资料