学完 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 学习助手:
- system 提示词设为 Python 助教。
- 页面显示历史消息。
- 每次输入后调用模型回答。
- 会话保存到
session.json。 - 增加一个清空会话按钮。
清空按钮可以这样写:
python
if st.button("清空会话"):
st.session_state.messages = [
{"role": "system", "content": "你是一个耐心的 Python 学习助手。"}
]
save_messages(st.session_state.messages)
参考资料
- OpenAI SDKs and CLI:https://developers.openai.com/api/docs/libraries
- Streamlit 文档:https://docs.streamlit.io/get-started
- Streamlit Session State:https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state