
想让自己的程序接上大模型,光拿到一个 API Key 远远不够:模型是本地部署还是调云端接口?HTTP 请求该怎么发、参数和响应长什么样?为什么多聊两句 AI 就"失忆"、答非所问?聊天界面是不是还得先学前端?程序一关,之前的对话记录全没了怎么办?这些问题不解决,代码跑通也只是"能跑",离一个能用的东西还差得远。
为了解决这个问题,Python 提供了一套开箱即用的组合:用 OpenAI 兼容的 SDK 或 requests 直接调用大模型开放 API,用 messages 数组滚雪球式拼接历史消息补上会话记忆,用 Streamlit 在几十行代码内搭出可用的 Web 聊天界面,用标准库 os、json、datetime 把会话数据持久化到本地文件,再配合提示词工程让 AI 严格按预设人设回复——不需要写一行前端代码,就能把一个 AI 对话应用从零做到可交付。
本文基于黑马程序员 Python+AI 开发课程录音整理,按 Python 核心语法体系中「Python项目实战之AI应用」模块的授课顺序,系统梳理 AI 与大模型(LLM)基础概念、本地部署与官方开放 API 两类部署方案、IP/域名/端口与 HTTP 请求响应格式、大模型 API 调用与流式输出、提示词工程六大技巧、Streamlit 常用组件与页面配置,以及 AI 智能伴侣项目的完整开发过程(页面搭建、状态持久化、会话记忆、人设定制、新建/查询/加载/删除会话与 JSON 持久化),文末给出重构前后两套完整项目代码。
一、AI 应用基础:概念、部署方案与网络底层(第 1-8 节)
第 1 节 · AI与大模型及AI应用基础概念
AI 的定义与核心目标
AI(Artificial Intelligence,人工智能)是学科领域的统称,并非单一技术;其核心目标是让机器具备像人类一样独立思考、学习、推理以及解决问题的能力。AI 的四大核心能力维度为:思考、学习、推理、解决问题。
AI 实现智能的底层逻辑
人类智能依托人脑极其复杂的神经网络(海量神经元与突触结构)。机器通过代码结合特定数学算法,模拟人脑的神经网络结构,从而获得类似人类的智能能力。
AI 大模型(LLM)核心概念
- 别名与缩写:大语言模型,英文 Large Language Models,简称 LLM,是 AI 领域的通用高频术语。
- 本质:AI 技术的重要分支,本质是一段用代码模拟人脑神经网络的程序。
- "大"的体现:参数量极其庞大,通常达到数十亿至数千亿级别。
- 能力来源:经海量数据训练后,具备理解人类语言、独立思考、逻辑推理并输出符合人类语言习惯内容的能力。
主流大模型产品
| 海外 | OpenAI GPT 系列、Google Gemini、Anthropic Claude 系列 |
| 国内 | DeepSeek(深度求索)、Qwen(阿里巴巴)、字节豆包、科大讯飞星火、腾讯混元 |
AI 应用定义与场景
- 定义:将 AI 大模型技术落地到具体业务场景,用来解决实际问题的产品或服务。
- 场景:对话式问答工具、智能客服、视频内容 AI 摘要提取、AI 数字人直播、AI 短剧、电商 AI 购物助手、医疗智能诊断系统、金融量化交易平台等。
行业趋势与学习路径
- 趋势:国家大力发展人工智能,赋能千行百业,未来应用软件将依托 AI 大模型重构升级。
- 本章三部分:AI 应用基础、AI 应用实战、AI 应用知识扩展。
- 基础:大模型部署、大模型调用、提示词编写。
- 实战:基于 Python 开发 AI 智能伴侣,覆盖页面布局、伴侣个性化配置、提示词编写、会话管理(新建会话、查询历史会话、删除会话)。
- 知识扩展:补充进阶相关知识。
第 2 节 · 大模型部署三类方案与API概念
AI 应用基础三大核心
大模型部署、大模型调用、提示词工程。
大模型定义
通过代码、数据与算法模拟人脑神经网络的软件程序,必须完成部署后才能对外提供服务。
三类部署方案对比
| 本地部署 | 数据安全、自主可控(可微调)、长期成本低 | 初始硬件成本高、需长期运维、性能受限(仅能部署蒸馏/阉割版) |
| 官方开放 API | 前期成本低、无需部署维护、随时访问 | 隐私不能保障、长期成本高、可控性差 |
| 云服务平台 | 前期成本低、无需部署维护、选择度高(可调用多厂商模型) | 安全及隐私不能保障、长期成本高 |
API 概念
API(Application Programming Interface,应用程序编程接口)是不同软件/应用程序之间的标准化"桥梁",开发者无需了解内部实现细节,直接调用对外暴露的功能或数据即可。
云服务平台示例
阿里云百炼、百度千帆、字节跳动火山引擎等。官方开放 API 与云服务平台均为新用户提供一定免费调用额度。
课程学习范围
重点讲解本地部署、官方开放 API 两种方案;云服务平台使用逻辑与官方开放 API 高度相似,掌握前两种后可快速迁移上手。
第 3 节 · Ollama本地部署DeepSeek-R1
Ollama 基础
- 定位:本地运行、管理大语言模型的开源工具,官网 https://ollama.com/,支持 macOS、Windows、Linux。
- 注意:Ollama 本身只是运行大模型的载体,安装后本地不自带任何大模型,需后续手动下载部署。
两种安装方式
- 默认安装:双击官方 exe 安装包自动完成,默认路径为 C 盘用户目录;C 盘空间充足直接选此方式,操作门槛极低。
- 自定义安装:适用于 C 盘空间不足场景,必须通过命令行执行,不能直接双击 exe。
OllamaSetup.exe /DIR=D:/develop/ollama
- 环境变量:新增系统变量 OLLAMA_MODELS,值为模型存储目录,避免大模型占用系统盘空间。
核心命令
ollama –help # 查看所有支持的操作指令
ollama create # 创建一个自定义模型
ollama show # 查看指定模型的详细信息
ollama run # 运行指定大模型,本地不存在则自动联网下载
ollama stop # 停止当前正在运行的大模型
ollama list # 列出本地所有已下载的大模型信息
ollama ps # 查看当前正在运行的大模型
ollama pull # 从官方模型仓库拉取指定模型
ollama serve # 启动 Ollama 后台服务
开源模型选型规则
- 参数标识:模型名称后的 B 代表十亿参数,如 1.5B=15 亿参数、8B=80 亿参数、671B=6710 亿参数。
- 规律:参数量越大推理性能越强,但对算力、内存、显存要求越高;满血版 671B 模型需 8 张 H200 级别专业 GPU 才能流畅运行。
- 显存占用参考:1.5B 约 1.1GB、7B 约 4.7GB、8B 约 5.2GB。
- 选型建议:无独显办公本选 1.5B;搭载 8G 显存 RTX4070 级别的游戏本可流畅运行 8B。
DeepSeek-R1 部署与交互
- 部署命令(自动下载 5.2GB 模型文件后进入命令行交互界面):
ollama run deepseek-r1:8b
- 交互指令:直接输入问题即获得流式输出回复;输入 /? 查看交互帮助;输入 /bye 退出对话返回系统命令行。
- 本地管理:执行 ollama list 可查看模型名称、唯一 ID、文件大小、最后修改时间等完整信息。
第 4 节 · DeepSeek开放平台注册与API Key
两种部署方案回顾
- 本地部署:基于 Ollama 完成本地化部署,算力由本地设备提供。
- 官方开放 API:厂商将模型部署在自有服务器,用户无需本地部署直接调用,无需本地算力支撑,上手门槛极低。
DeepSeek 官网两大入口
- "开始对话":面向普通用户,可直接与 DeepSeek-V3.2 免费对话。
- "API 开放平台":面向开发者,用于自定义 AI 应用中调用大模型接口。二者功能完全不同,集成应用必须选第二个入口。
开放 API 调用逻辑
- 厂商侧:模型部署在厂商自有服务器,调用消耗厂商算力,按量计费。
- 用户侧:通过专属身份标识(API Key)发起请求,厂商校验身份、统计调用量并从账户扣费。
- API Key 是调用大模型的唯一身份凭证,所有权限校验、计费扣费均基于该标识完成。
四步准备流程
安全风险
API Key 泄露后,他人可直接使用你的账户余额调用大模型,产生非预期扣费。
第 5 节 · 网络基础:IP、域名与端口
大模型调用网络角色
服务端提供大模型服务并对外暴露 API 接口;客户端发起请求,须通过网络连接服务端才能调用。
互联网
由无数网络设备(电脑、手机、平板等)连接形成的全球性网络,核心作用是实现不同网络设备之间的数据传输。
IP 地址
- 定义:设备在互联网中的唯一地址("唯一身份证"),用于定位设备。
- IPv4:32 位二进制,日常展示为四段十进制,每段 0~255;示例 132.12.86.125。
- IPv6:128 位二进制,设计用于解决 IPv4 地址总量不足问题。
- 特殊本地回环地址:127.0.0.1,代表当前运行程序的本机设备,常用于本地开发调试。
域名与 DNS 解析
- 域名:由点分隔的英文字母组成的标识,解决 IP 纯数字难以记忆的痛点。
- 解析流程:用户输入 www.baidu.com → 向 DNS 服务器请求解析 → 获取目标 IP 110.242.69.21 → 访问该 IP 对应服务器。
- 关键点:域名本身不能直接定位设备,必须经 DNS 解析转换为 IP 地址后才能发起网络请求。
端口号
- 定义:取值范围 0~65535 的整数,标识同一台网络设备上正在运行的不同应用程序,单台设备内端口号不重复。
- 默认端口:HTTP=80,HTTPS=443;未手动指定端口时浏览器自动使用对应协议默认端口。
- 示例:同一 IP 110.242.69.21 的服务器上,百度搜索端口 443、百度网盘 10010、爱奇艺 9527、百度地图 8848。
核心概念对比
| IP | 唯一定位互联网设备 | IPv4(32位)、IPv6(128位);本机 127.0.0.1 |
| 域名 | 简化 IP 记忆,通过 DNS 映射 | 点分结构;本机 localhost |
| 端口 | 标识设备内运行的程序 | 范围 0~65535;默认 80/443 |
完整访问链路
输入带协议的域名 → DNS 解析得目标 IP → 浏览器补全协议默认端口 → 通过"IP+端口"精准定位目标应用 → 应用处理完成返回结果。
第 6 节 · 网络分层模型与HTTP协议
网络定位回顾
IP 地址定位终端设备,端口号定位设备上运行的具体应用程序,二者缺一不可。
标准化网络模型
- 制定主体:国际标准化组织 ISO。
- 两类模型:OSI 七层网络模型(全球标准参考)、TCP/IP 四层网络模型(工业简化版本,实际生产环境几乎全部使用)。
模型对照
| 应用层 | 应用层 | 按 HTTP/FTP/SMTP 等协议封装用户数据 |
| 表示层 | 应用层 | (合并入应用层) |
| 会话层 | 应用层 | (合并入应用层) |
| 传输层 | 传输层 | 通过端口定位应用进程;含 UDP、TCP |
| 网络层 | 网络层 | 封装源/目标 IP,路由寻址 |
| 数据链路层 | 网络接口层 | (合并,处理物理网络传输) |
| 物理层 | 网络接口层 | (合并,处理与硬件交互) |
分层优势
类似工厂流水线,各层职责独立,出现故障可快速定位到对应层级排查。
各层职责
- 应用层:面向用户交互,使用 HTTP、FTP、SMTP 等协议封装数据,调用传输层 API 移交下一层,无需关心底层。
- 传输层:通过端口号将数据包精准送达目标应用。UDP(速度快、开销小、不保证可靠,适用于视频/语音通话);TCP(面向可靠连接、带确认机制,适用于转账等不允许丢数据场景)。
- 网络层:封装源 IP 与目标 IP,依靠 IP 地址完成跨网络路由。
- 网络接口层:处理物理网络中的数据传输,实现数据与底层硬件交互。
- 开发人员仅需关注应用层,底层三层由操作系统内核自动完成封装。
HTTP 协议
- 全称:Hypertext Transfer Protocol(超文本传输协议),属于 TCP/IP 模型应用层协议。
- 作用:统一规定客户端与服务器之间数据传输的格式规则,双方遵循同一规则才能互相解析。
- 三大核心特点:
- 基于文本:请求与响应内容均为文本格式,底层依托 TCP 协议传输,稳定性强。
- 基于请求-响应模型:一次客户端请求严格对应一次服务器响应,请求由客户端主动发起,服务器不主动返回。
- 无状态:服务器不存储客户端历史交互信息,每次请求-响应完全独立。
第 7 节 · HTTP请求响应格式与GET/POST
请求数据三段式结构
POST /api/courses HTTP/1.1
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json
Host: localhost:90
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/143.0.0.0
{"phone":"18808088080","channel":1,"name":"虎咆","gender":1,"age":"32"}
GET 与 POST 对比
| 参数位置 | 请求行 URL 中,如 /api/courses?name=Python&status=1 | 请求体中 |
| 参数大小 | 浏览器有限制(通常几 KB),不可传大体积数据 | 无限制,适合文件上传、大体积传输 |
| 请求体 | 无 | 可携带 |
浏览器抓包验证
- 打开方式:F12 → Network(网络)面板;勾选 Raw 查看最原始 HTTP 文本格式。
- GET 抓包:有请求行、自动生成的请求头,无请求体,参数显示在 URL 问号之后。
- POST 抓包:请求行方式为 POST;Chrome 将请求体单独放置在 Payload 标签页,需点击 Raw 查看原始数据。登录操作属 POST,用户名密码等敏感数据在请求体中传输。
响应数据三段式结构
高频状态码
| 200 | 请求成功 | 成功 |
| 400 | 请求参数不符合服务器要求 | 4 开头 = 客户端错误 |
| 404 | 请求资源不存在(URL 拼写错误或资源已删) | 4 开头 = 客户端错误 |
| 500 | 服务器内部未预期异常 | 5 开头 = 服务器错误 |
格式小结
- 请求:请求行(方式、路径、版本)→ 请求头(key:value)→ 空行 → 请求体(仅 POST 可携带)
- 响应:响应行(版本、状态码)→ 响应头(key:value)→ 空行 → 响应体
第 8 节 · Apifox接口测试与DeepSeek API调用
浏览器请求局限
浏览器地址栏发起的所有请求默认均为 GET,参数拼在 URL 后方且大小受限。大模型交互传递的提示词可达几 KB 甚至数百 KB,因此 DeepSeek 官方 API 仅支持 POST,无法通过浏览器地址栏直接调用。
Apifox 工具
- 定位:API 设计、开发、测试一体化协作平台,同类工具还有 Postman。
- 功能:API 开发调试、API Mock、API 文档生成、API 自动化测试。
- 使用:官网按系统类型下载安装包,安装后微信扫码登录,新建专属测试项目。
DeepSeek API 基础信息
- 兼容特性:完全兼容 OpenAI 接口规范,可直接使用 OpenAI SDK。
- 请求 URL:https://api.deepseek.com/chat/completions
- 认证方式:请求头 Authorization 字段携带 API Key。
- 支持模型:deepseek-chat(非思考模式)、deepseek-reasoner(思考模式),均已升级为 DeepSeek-V3.2(目前已有变化,可以根据官方说明做调整)。
JSON 数据格式
- 全称 JavaScript Object Notation,形式类似 Python 字典,采用 key-value 结构。
- 语法:所有 key 必须用双引号包裹;布尔值 true/false 首字母小写且不能加引号,否则触发语法校验错误。
- 类型:对象 {}、数字(整数小数直接写)、字符串 ""、布尔值、列表 []。
Apifox 调用配置
- 请求方式设为 POST,粘贴官方请求 URL。
- 请求头:Content-Type: application/json、Authorization: Bearer 你的API Key。
- Body 选择 JSON 格式,粘贴请求体模板。
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
],
"stream": false
}
- model:指定本次调用的大模型名称。
- messages:存储交互历史消息列表。
- stream:false 代表非流式输出,生成完所有内容后一次性返回。
消息角色定义
- system:设定 AI 身份、行为准则、回答风格与限制(固定人设)。
- user:用户实际发送的问题或指令。
- assistant:大模型返回的响应内容。
响应结果解析
- choices[0].message.role 为 assistant,对应 content 字段即大模型生成的回答内容。
- usage 统计 token 消耗:prompt_tokens(输入提示词)、completion_tokens(生成回答)、total_tokens(总计)。
curl 调用示例
curl https://api.deepseek.com/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${DEEPSEEK_API_KEY}" \\
-d '{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
],
"stream": false
}'
二、大模型调用与提示词工程(第 9-12 节)
第 9 节 · 大模型无状态与滚雪球记忆
大模型 API 单次交互基础
基于 DeepSeek 官方开放 API 构建单次对话请求,通过 system 角色设定 AI 助手人设与回复风格,user 角色传入用户提问。初始请求结构示例如下:
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一名非常可爱的AI助理,你的名字叫小甜甜,请你使用温柔可爱的语气回答用户的问题"},
{"role": "user", "content": "你是谁"}
],
"stream": false
}
接口返回状态码 200,大模型返回符合人设的自我介绍内容,单次交互测试通过。
无状态交互的问题复现
第一轮发送「12个苹果,3个人怎么均分」,大模型返回 12÷3=4,每人分到 4 个苹果。第二轮单独发送「那两个人呢?」,由于未携带任何历史上下文,大模型回复「你是指哪两个人呢?你可以告诉我更多的细节吗?」,无法给出正确计算结果。
在浏览器中使用官方对话产品发送相同两个问题,官方产品可正确关联上下文,第二次提问自动理解为「12个苹果分给两个人」,返回每人分 6 个的结果。差异原因在于:官方对话产品在后台封装了会话记忆逻辑,自动维护历史对话上下文;而直接调用原生 API 时,每一次请求相互独立,默认不携带任何历史交互信息。
无状态本质
与 AI 大模型的交互本质是无状态的,每一次请求与响应相互独立,大模型本身没有真正的会话记忆能力,无法自动留存之前的对话内容。原生 API 不会自动保存任何历史对话,所有上下文信息都需要开发者主动通过请求参数传递。
会话记忆滚雪球方案
每次发起新的 API 请求时,把之前所有的历史对话内容(包括 system 设定、用户提问、大模型回复)全部追加到 messages 数组中,像滚雪球一样不断累加上下文,让大模型获取完整的对话历史。这是当前绝大多数 AI 应用实现会话记忆最基础的通用方案。
多轮对话的 messages 结构示例:
"messages": [
{"role": "system", "content": "你是一名可爱的AI助手,你的名字叫小甜甜,请以亲切、可爱语气来回答用户的问题"},
{"role": "user", "content": "12个苹果,3个人怎么均分?"},
{"role": "assistant", "content": "嘻嘻,这个问题很简单哦!12个苹果分给3个人,每个人可以分到 **4个苹果** 哦~ 算式是: 12 ÷ 3 = 4"},
{"role": "user", "content": "那2个人呢?"},
{"role": "assistant", "content": "哎呀,如果是2个人的话,每个人可以分到 **6个苹果** 呢~ 算式是: 12 ÷ 2 = 6 "},
{"role": "user", "content": "那4个人呢?"}
]
将第一轮用户提问与大模型回复追加到 messages 数组后再传入「那两个人呢?」,大模型成功返回每人分 6 个;继续追加上一轮回复并传入「那六个人呢?」,大模型返回 12÷6=2,每人分 2 个,验证会话记忆完全生效。
stream 流式输出参数
stream 参数控制大模型输出模式,对比如下:
| false | 非流式输出 | 等待大模型生成完整内容后一次性返回 |
| true | 流式输出 | 大模型一个字一个字持续向外返回内容 |
开启流式输出后,接口持续返回分段内容,在接口测试工具中点击「自动合并」即可将分段内容拼接为完整回复文本。JSON 格式中布尔值 true 必须为全小写,写成大写会触发格式错误。流式输出适合在前端实现打字机效果。
messages 数组三种角色
| system | 设定 AI 身份和行为准则,可自定义回答风格、内容限制、回复规则,相当于初始人设 |
| user | 代表用户实际提出的问题或指令,是向大模型传递的核心交互内容 |
| assistant | 代表 AI 返回的回复内容,需追加到后续请求的 messages 数组中,实现上下文连贯传递 |
关键参数与响应解析
| model | 模型标识 | deepseek-chat |
| stream | 是否流式返回结果 | false |
| messages | 对话消息数组(含历史记录) | [{"role":"system",…},…] |
响应结构包含 id(请求 ID)、object(对象类型,如 chat.completion)、created(时间戳)、choices(回复内容数组),其中 choices[0].message.content 为 AI 的具体回复。
第 10 节 · Ollama本地模型API调用
本地大模型启动
部署不同参数量的 DeepSeek 模型时,启动命令需对应版本后缀:部署 8B 版本使用 deepseek-r1:8b,部署 1.5B 版本使用 deepseek-r1:1.5b;不清楚已部署版本时执行 ollama list 查看所有本地模型列表。核心启动命令为 ollama run <模型名>,执行后进入命令行交互式对话界面。
Ollama 开放 API 基础
Ollama 默认在本地 11434 端口对外提供 HTTP 服务,可通过 http://localhost:11434 访问;也可替换为局域网 IP 地址(如 192.168.0.1),实现局域网内其他设备调用。在 Ollama 官网的 API Reference 板块可查看所有接口定义,对话核心接口为 Generate a chat message。
Chat 接口请求参数
根据 API 文档,核心必填字段包括:
- model:指定调用的本地大模型名称
- messages:消息对象数组,每个对象必须包含 role(可选值 system、user、assistant、tool)和 content 字段
可选扩展参数:messages.images 字段传入 Base64 编码的图片内容,适配多模态模型;messages.tool_calls 字段用于传递工具调用请求。
示例 curl 请求:
curl http://localhost:11434/api/chat -d '{
"model": "gemma3",
"messages": [
{
"role": "user",
"content": "why is the sky blue?"
}
]
}'
API 客户端调用实战
使用 POST 方法访问 http://localhost:11434/api/chat,请求体选择 JSON 格式,设置 stream: false 关闭流式响应。自定义角色对话请求示例如下:
{
"model": "deepseek-r1:8b",
"messages": [
{"role": "system", "content": "你是一名非常可爱的AI助理,你的名字叫小菲菲,请你使用温柔可爱的语气回答用户的问题"},
{"role": "user", "content": "12个苹果,3个人,怎么均分?"}
],
"stream": false
}
返回 JSON 中包含 thinking 字段,对应大模型的内部思考过程,可根据业务需求选择保留或忽略。
对话上下文记忆
若第二次请求仅发送新问题「两个人呢?」,大模型会丢失之前「12个苹果均分」的上下文,返回不符合预期结果。必须在后续请求的 messages 数组中追加之前所有历史交互记录(用户提问与大模型回复)才能实现多轮对话记忆。带上下文的请求示例:
{
"model": "deepseek-r1:8b",
"messages": [
{"role": "system", "content": "你是一名非常可爱的AI助理,你的名字叫小菲菲,请你使用温柔可爱的语气回答用户的问题"},
{"role": "user", "content": "12个苹果,3个人,怎么均分?"},
{"role": "assistant", "content": "呀!这个问题好简单哦!让我们来均分一下吧~ 12个苹果,3个人,我们可以这样分:\\n每个人得到 **12 ÷ 3 = 4(个)** 的苹果哦!\\n所以,每个人分到 **4个苹果**,这样大家都一样多啦! 😊\\n嘿嘿,这样大家都开心,对不对?"},
{"role": "user", "content": "两个人呢?"}
],
"stream": false
}
本地与官方大模型调用共性
本地私有大模型与官方开放大模型的请求参数结构、消息格式完全一致,唯一区别仅为请求的 URL 地址不同,开发者可无缝切换两套服务,无需大规模修改业务代码。
核心概念
| Ollama 默认端口 | 11434,本地大模型对外暴露 API 的默认访问端口 |
| Chat 接口核心请求体 | {"model":"模型名称","messages":[{"role":"角色","content":"消息内容"}],"stream":false} |
| thinking 字段 | DeepSeek 系列大模型返回的内部思考过程字段,模型特有返回内容 |
多轮对话示例:
| 1 | system | 你是一名非常可爱的AI助理,你的名字叫小菲菲,请你使用温柔可爱的语气回答用户的问题 |
| 2 | user | 12个苹果,3个人,怎么均分? |
| 3 | assistant | 呀!这个问题好简单哦!让我们来均分一下吧~🍎🍎\\n\\n十二个苹果,3个人,我们可以这样分:\\n\\n每个人得到 12 ÷ 3 = 4(个) 的苹果哦!\\n\\n所以,每个人分到 4个苹果,这样大家都一样多啦!😊\\n\\n嘿嘿,这样大家都开心,对不对? |
| 4 | user | 两个人呢? |
| 5 | assistant | 嗯呢,两个人的话,更简单啦!😉\\n\\n12个苹果,两个人分:\\n\\n12 ÷ 2 = 6(个)\\n\\n所以,每个人可以得到 6个苹果!\\n\\n这样,两个人都分到相同数量的苹果,大家都很开心呢~🍎\\n\\n啦啦啦~你算对了吗?太棒啦!💖 |
第 11 节 · Python调用大模型与pip管理
官方支持与 Python 模块分类
DeepSeek 官方提供 Python 和 Node.js 两种调用语言,Python 是 AI 开发首选。Python 模块分为两类:
- 标准库:内置提供,无需安装,如 os、math、random、csv、re 等
- 第三方模块:由社区或企业维护,需手动安装,如 openai、requests、numpy 等
PyPI 与 pip
PyPI(Python Package Index)是由 Python 官方和社区共同维护的第三方软件包官方仓库,目前维护超过 71 万个项目。pip 是 Python 官方提供的包管理工具,支持第三方包的查找、下载、安装、卸载等全生命周期管理。若仅安装一个 Python 3 版本,pip 与 pip3 通用;多版本共存时 pip3 专门管理 Python 3 的包。pip 命令执行时自动联网从 PyPI 下载。
openai 库安装
在 PyCharm 终端执行以下命令:
pip install openai
终端输出以 "successful" 开头的提示信息即代表安装完成。代码中 import 语句出现黄色高亮属于 PyCharm 语法提示配置问题,并非代码错误。
DeepSeek 调用代码解析
导入依赖模块:
import os
from openai import OpenAI
os 是标准库,用于读取系统环境变量;OpenAI 从第三方 openai 库导入。
创建客户端对象:
client = OpenAI(api_key=os.environ.get('DEEPSEEK_API_KEY'), base_url="https://api.deepseek.com")
通过环境变量读取 API Key 避免硬编码泄露,base_url 指定 DeepSeek 官方接口地址。
发起对话请求:
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hello"},
],
stream=False
)
解析并输出响应结果:
print(response.choices[0].message.content)
API Key 环境变量配置
在 Windows「高级系统设置-环境变量」中新建名为 DEEPSEEK_API_KEY 的用户变量,变量值填写自己的 DeepSeek API Key。配置完环境变量后必须重启 PyCharm,新环境变量才会加载生效,否则会出现 401 未认证错误。
常见报错排查
401 Authentication error 代表身份认证失败,核心原因是 API Key 不合法,最常见场景是配置完环境变量后未重启开发工具,导致程序未读取到最新 API Key。
pip 常用命令
| 安装最新版本 | pip install 包名 |
| 安装指定版本 | pip install 包名==版本号 |
| 卸载已安装包 | pip uninstall 包名 |
| 列出已安装包 | pip list |
| 查看包详情 | pip show 包名 |
第 12 节 · 提示词工程六大技巧
提示词基础概念
提示词(Prompt)是引导大模型(LLM)进行内容生成的命令,可以是一句话、一个问题、一段话。日常和大模型交互发送的所有指令都属于提示词范畴,如「给我讲个故事」「用Python写一个游戏」「法国大革命爆发的原因」。
劣质 vs 优质提示词
| 特征 | 仅给出模糊核心诉求,未限定输出范围、受众、形式 | 引导 AI 思考,明确核心任务,约束输出范围,明确期望结果 |
| 实测(仅输入「法国大革命爆发的原因」) | 大模型自动从根本原因、思想启蒙、直接诱因、民众起义、外部因素五个维度展开,结构、语气、详略不受控制 | 大模型严格按指定角色、结构、格式生成,完全匹配定制化需求 |
提示词工程(Prompt Engineering)是通过有技巧地编写提示词,让大模型生成内容尽可能符合用户预期的持续性过程,不是一次性操作,需要反复调试迭代。
六大核心技巧
| 1 | 给大模型设定角色与能力 | 「你是一名经验丰富的高中历史老师,擅长生动有趣的讲解复杂的历史事件」 |
| 2 | 明确核心请求与任务 | 「请向一位高中生解释法国大革命爆发的主要原因」 |
| 3 | 按步骤拆解复杂任务 | 「请先概述背景,然后分政治、经济、思想三个方面阐述,每点配一个例子」 |
| 4 | 指定风格与语气 | 「请使用简洁、口语化、充满热情的语气,避免使用过于学术的术语」 |
| 5 | 明确要求输出格式 | 「请严格按照如下结构输出:…」 |
| 6 | 提供输入输出的示例 | 「可以参考《人类群星闪耀时》的叙事风格。请不要列出冗长的日期列表」 |
六大技巧不需要在每次编写提示词时全部覆盖,可根据业务需求灵活调整。
实战案例:法国大革命主题优质提示词
完整提示词内容如下:
你是一名经验丰富的历史老师,擅长用生动有趣的方式讲述复杂的历史事件。现在,请完成以下任务:
1. 核心任务:向一名高中生解释法国大革命爆发的主要原因。
2. 表达要求:
语气与风格:使用简洁、口语化、充满课堂热情的语气,避免学术黑话。整体叙事可以借鉴《人类群星闪耀时》中对历史关键时刻的描写手法,富有画面感和戏剧性。
内容禁忌:聚焦于原因分析,不要罗列冗长的日期和事件过程表。
3. 内容与结构要求:
开头:先用一段话,生动描述革命前法国社会的总体氛围和紧张感(即背景概述)。
主体:分别从政治、经济、思想三个层面阐述直接原因。每个层面提炼2个关键点,并为每个关键点配1个具体、有说服力的史实例子。
结尾:提供一个帮助学生记忆的妙招。
4. 输出格式:请严格、完整地按照以下框架组织你的回答:
【历史现场氛围】
(在这里写你的背景概述)
【危机根源解析】
政治层面:
– 关键点1:… 例子:…
– 关键点2:… 例子:…
经济层面:
– 关键点1:… 例子:…
– 关键点2:… 例子:…
思想层面:
– 关键点1:… 例子:…
– 关键点2:… 例子:…
【记忆法宝】
(请提供一个巧妙的比喻、口诀或联想图像,将这三大层面串联起来,方便学生瞬间记忆)
使用优化后提示词,大模型严格按照指定角色、结构、格式生成内容,分为历史现场氛围、危机根源解析、记忆法宝三部分,与仅输入「法国大革命爆发的原因」的输出结果差异极大。
核心凝练:定角色、给任务、提要求
| 定角色 | 给大模型设定角色与能力 |
| 给任务 | 明确核心请求与任务、按步骤拆解复杂任务 |
| 提要求 | 指定风格与语气、明确要求输出格式、提供输入输出的示例 |
关键原则
- 精准性:避免模糊表述,明确任务边界(如「不要列出冗长日期」)
- 结构化:通过分点、分层拆解复杂任务
- 示例化:提供参考案例帮助对齐输出风格
- 迭代优化:通过持续调整提示词逐步逼近理想输出
三、Streamlit:不会前端也能搭 Web 界面(第 13-16 节)
第 13 节 · AI智能伴侣与Streamlit入门
AI 智能伴侣项目介绍
项目核心功能:支持自定义 AI 伴侣的名字与性格,实现带记忆的对话交互,提供新建、保存、加载、删除会话的完整会话管理,最终交付带图形界面的桌面级交互产品。界面分为左右两栏:左侧为 AI 控制面板(新建会话按钮、历史会话列表、自定义名字与性格配置区),右侧为对话展示区(区分用户与 AI 对话内容,底部输入框发送提问)。
Streamlit 基础概念
Streamlit 是一款开源 Python 库,专为数据工程师和机器学习工程师设计,无需掌握 HTML、CSS、JS 等前端技术,仅通过 Python 代码就能快速构建交互式 Web 网站。官方网站为 https://streamlit.io,配套完整英文开发文档,建议优先阅读英文原版,避免机翻导致语义偏差。Streamlit 仅适用于数据科学、机器学习、AI 应用这类轻量交互场景,无法构建功能复杂的大型网页。
安装
Streamlit 属于第三方 Python 库,通过 pip 安装,会自动同步下载 numpy、pandas 等依赖包:
pip install streamlit
标准开发四步流程
基础文本 API
| st.title() | 页面最大的主标题 |
| st.header() | 一级标题 |
| st.subheader() | 二级标题 |
入门演示代码:
import streamlit as st
# 大标题
st.title("Streamlit 入门演示")
st.header("Streamlit 一级标题")
st.subheader("Streamlit 二级标题")
运行与效果验证
在项目目录终端执行 streamlit run 你的文件名.py,程序默认占用 8501 端口,自动打开浏览器访问 http://localhost:8501。点击界面右上角功能菜单进入设置,可切换亮色/暗色主题。下一小节将演示 st.write()、st.image()、st.divider()、st.table() 及音频、视频元素的嵌入。
第 14 节 · Streamlit常用组件详解
基础导入
import streamlit as st
导入语句是所有 Streamlit 项目的第一行代码,不能省略。
标题组件
st.title("Streamlit入门演示")
st.header("Streamlit一级标题")
st.subheader("Streamlit二级标题")
标题组件自动渲染为网页标题样式,无需额外编写 HTML 标签。
段落文字:st.write
st.write 支持字符串、字典、列表等多种数据类型,传入字符串时渲染为网页段落,连续调用生成多个独立段落:
st.write("布偶猫,被誉为\\"猫中仙女\\",以其优雅的外表和温顺的性格成为最受欢迎的宠物猫之一。")
st.write("它们体型较大,成年后可达10-20斤,拥有深邃的蓝色眼眸和柔顺的中长毛,毛色多为双色、手套色或重点色,宛如戴着一副可爱的面具。最迷人的是其松弛柔软的体态")
st.write("性格是布偶猫最大的魅力。它们极度温顺、安静且粘人,被称为\\"小狗猫\\",喜欢跟随主人走动,享受陪伴。它们通常脾气极好,忍耐力强,能与儿童和其他宠物友好相处")
st.write("养护方面,需要定期梳理其丰厚毛发以防止打结,并提供足够的关注与互动。拥有一只布偶猫,就如同拥有了一位温柔优雅、终生依恋的毛茸茸家人。")
图片:st.image
核心必填参数为图片资源路径,支持 width 可选参数自定义宽度,页面自带点击放大预览。./ 代表当前代码文件所在目录,也可省略直接写相对路径。
st.image("./resources/益生菌.jpg")
音频:st.audio
仅需传入音频文件相对路径,自动生成带播放、音量调节功能的播放器。
st.audio("resources/当你翻开这本书,你已经开始用知识指导生活了.MP3")
视频:st.video
支持传入视频路径,通过 width 参数自定义播放器宽度,页面自带全屏控制。
st.video(data="resources/C003.mp4", width=300)
全局 Logo:st.logo
渲染后固定显示在页面底部左上角。不要用 st.image 设置 Logo,必须使用 st.logo 才能实现全局固定效果。
st.logo("resources/logo.png")
表格:st.table
接收字典类型数据,字典键自动作为表头,对应值为列表类型存储每列单元格数据。
student_data = {
"姓名": ["王林", "李慕婉", "贝罗", "莫厉海", "石萧"],
"学号": ["20260001", "20260002", "20260003", "20260004", "20260005"],
"语文": [98, 90, 59, 29, 80],
"数学": [88, 78, 65, 70, 39],
"英语": [99, 89, 87, 59, 62],
"总分": [285, 257, 211, 158, 181]
}
st.table(student_data)
文本输入框:st.text_input
第一个参数为提示标签,返回值直接获取用户输入;输入内容不会实时触发页面更新,必须按下回车键后绑定的变量才拿到最新值。传入 type="password" 可转换为密码输入框。
name = st.text_input("请输入姓名")
st.write(f"您输入的姓名为: {name}")
password = st.text_input(label="请输入密码", type = "password")
st.write(f"您输入的密码为: {password}")
单选按钮:st.radio
第一个参数为提示标签,第二个参数 options 传入选项列表,第三个参数 index 设置默认选中项下标(从 0 开始计数)。
sex = st.radio(label="请输入您的性别:", options = ["男","女","未知"], index=1)
st.write(f"你的性别是: {sex}")
组件通用开发套路
所有 Streamlit 组件用法都可在官方文档和配套参考手册中查询,统一流程为:查阅文档 → 复制示例代码 → 修改资源路径和自定义参数 → 刷新页面查看效果。
第 15 节 · st.set_page_config全局配置
默认布局与配置入口
未配置时网页内容仅居中展示,左右两侧保留等距空白,无法铺满视口。st.set_page_config 代码必须放在整个 Python 脚本最顶部、所有页面渲染组件之前,否则部分配置不生效。在官方文档 Develop 板块 API 栏目的「配置」分类可查看相关说明。
核心配置项
| page_title | 浏览器标签栏显示的网页标题 | 替代默认 "Streamlit" 字样 |
| page_icon | 浏览器标签栏小图标 | 支持本地图片路径或 emoji 符号 |
| layout | 页面整体布局范围 | centered(默认,居中留白)/ wide(铺满视口);第三种取值未展开说明 |
| initial_sidebar_state | 首次加载侧边栏状态 | expanded(默认展开)/ collapsed(默认收起) |
| menu_items | 右上角自定义菜单 | 字典格式,覆盖 Get Help、Report a bug、About 三项 |
layout 设为 wide 是数据可视化类 Streamlit 应用的常用配置。menu_items 中 About 项支持直接传入 Markdown 语法字符串,点击后页面内弹窗展示,无需跳转外部链接。
完整配置代码
import streamlit as st
# 页面设置
st.set_page_config(
page_title="Strealit入门",
page_icon="resources/logo.png",
layout="wide",
initial_sidebar_state="expanded",
menu_items={
'Get Help': 'https://www.extremelycoolapp.com/help',
'Report a bug': "https://www.extremelycoolapp.com/bug",
'About': "# This is a header. This is an *extremely* cool app!"
}
)
# 大标题
st.title("Streamlit入门演示")
st.header("Streamlit一级标题")
拍图总结中提供的等价配置样例(About 内容略有不同):
st.set_page_config(
page_title="Streamlit入门",
page_icon="resources/logo.png",
layout="wide",
initial_sidebar_state="expanded",
menu_items={
'Get Help': 'https://www.example.com/help',
'Report a bug': 'https://www.example.com/bug',
'About': "# 应用说明\\n这是一个Streamlit演示应用!"
}
)
执行方式:streamlit run 02.streamlit入门.py,自动启动本地 Web 服务并打开浏览器。
第 16 节 · 通义灵码插件安装使用(已经收费,可以换成CodeBuddy有免费额度,积分与WorkBuddy共用)
AI 编程助手引入背景
Python 基础语法学习阶段不使用 AI 编程工具,目的是夯实基本功,避免依赖 AI 生成代码而忽略底层逻辑。完成基础语法学习与练习后,在实战案例开发中引入 AI 编程助手生成基础代码提升效率。使用 AI 生成代码时若出现问题,需凭借已掌握的基本功自主排查调整,不能完全依赖 AI 输出。课程选用完全免费的阿里系 AI 编程产品,同类工具使用逻辑大同小异。
PyCharm 中安装通义灵码
进入路径:PyCharm 左上角「文件」菜单 → 「设置」 → 左侧「插件」栏目 → 插件市场。PyCharm 自带默认插件列表中没有该工具,必须进入插件市场搜索「通义灵码」,找到阿里巴巴发布的智能编码辅助工具后点击安装,等待联网下载完成(安装包约 24.5M,下载量超 2500 万,支持 VSCode、PyCharm 等多款主流工具)。安装完成后重启 PyCharm 使插件生效,重启后界面新增通义灵码功能提示。
登录激活
重启后右侧边栏出现通义灵码专属面板。登录渠道包括阿里云账号密码、手机号验证码,以及支付宝、钉钉、淘宝账号扫码登录;无阿里云账号的个人用户推荐直接使用支付宝扫码登录。插件必须完成登录授权后才能正常使用所有功能,未登录状态无法触发 AI 代码生成与问答服务。
基础功能演示
登录后在右侧交互面板输入自然语言问题,AI 可快速返回概念解释并附带入门示例代码。输入「什么是字符串」,插件自动返回字符串定义说明及可直接运行的入门示例代码。插件还支持代码智能生成、多文件修改、编程智能提醒等进阶功能,后续在 AI 智能翻译项目实战中详细演示。
后续安排
所有学习者需课后独立完成通义灵码插件安装与登录。下一次课程正式启动 AI 智能翻译实战项目开发,借助已配置好的 AI 编程助手提升开发效率。
核心概念
| 通义灵码 | 阿里巴巴发布的免费智能编码辅助工具,支持代码智能生成、智能问答、多文件修改、编程智能提醒,适配 VSCode、PyCharm 等 |
| AI 辅助开发切换节点 | 完成 Python 基础语法系统学习后,从手动编写切换为 AI 助手辅助开发,兼顾基本功巩固与开发效率 |
四、AI 智能伴侣项目实战(第 17-31 节)
第 17 节 · 项目需求拆解与聊天组件搭建
项目需求与页面布局
AI 智能伴侣项目页面分为左侧侧边栏与右侧核心展示区两大模块。侧边栏用于管理 AI 控制面板、历史会话、自定义 AI 伴侣的名字与性格;右侧核心区用于展示对话交互内容。核心功能包括:新建会话、历史会话保存/查询/删除、自定义 AI 伴侣人设、用户输入提示词、大模型返回响应、多轮对话展示。开发遵循"先基础后进阶"顺序:优先完成右侧核心对话区布局,再处理左侧侧边栏的会话管理功能。
Streamlit 页面基础配置
通过 st.set_page_config() 配置页面标题、页面图标、布局模式,将布局设置为 wide 实现全屏展示。页面图标推荐使用 emoji 大全网站挑选 emoji 直接复制。使用 st.title() 设置页面大标题,通过 st.logo() 引入本地资源目录下的 logo 图片完成品牌标识添加。页面配置代码必须放在所有 Streamlit 组件代码的最顶部,否则会触发框架报错。
聊天输入框与消息展示组件
st.chat_input() 用于生成聊天输入框,可自定义输入框内的提示占位文本,返回值为用户输入的字符串内容。直接对返回的字符串变量做布尔判断——非空字符串自动转为 True、空字符串转为 False,无需额外调用判空方法。该组件所有参数均带默认值,仅传入占位提示文本即可快速实现可用输入框。
st.chat_message(name) 用于生成专属消息气泡,通过 name 参数区分消息发送方:user 标识用户发送的消息,assistant 标识 AI 大模型返回的消息,框架自动生成对应不同的头像图标区分双方。该组件与 st.chat_input() 必须配合使用,才能实现符合常规聊天产品交互逻辑的页面效果。
# 页面基础配置
st.set_page_config(page_title="AI智能伴侣", page_icon="✨", layout="wide")
st.title("AI智能伴侣")
# 聊天输入框:返回用户输入字符串
prompt = st.chat_input("请输入您要问的问题")
# 消息展示:name 区分发送方
st.chat_message("user").write(prompt)
st.chat_message("assistant").write("这里是AI回复")
OpenAI 接口对接与单轮对话
直接复用之前项目中已编写完成的 OpenAI 客户端初始化代码,无需重复编写鉴权逻辑。将系统提示词单独定义为独立变量,后续可直接修改该变量实现 AI 伴侣性格的自定义配置;用户提示词直接绑定用户输入内容。使用 Python 内置 print() 将大模型返回结果输出到终端,方便排查接口调用异常。调用大模型时,必须将动态获取的用户输入内容传入 messages 参数,不能使用写死的测试提示词。
当前效果与遗留问题
运行项目后输入问题,可正常获取大模型返回的响应内容,用户消息与 AI 消息分别以不同气泡样式展示。连续发送多条消息时,旧的对话内容会被新内容覆盖,无法实现多轮对话的历史消息滚动展示——该问题下节课讲解解决方案。
重点速览与拍图要点
- 开发顺序:先完成右侧核心对话区布局,再处理左侧侧边栏会话管理。
- 页面配置代码必须置于所有组件代码最顶部。
- st.chat_input() 所有参数带默认值,仅传占位提示文本即可。
- Python 隐式类型转换:字符串非空为 True、空为 False,可直接作 if 判断条件。
- st.chat_message() 必传 name,user/assistant 差异化展示。
- 系统提示词变量化,便于切换 AI 性格。
- 拍图总结:双区域布局(左侧 AI 控制面板 + 右侧核心交互区);气泡式消息区分用户(橙底)与 AI(红底);交互入口为底部"请输入您的问题…"文本框。
第 18 节 · 重执行机制与状态持久化
消息覆盖问题现象
在已实现基础交互的页面中,每发送一条新提示词,页面原有聊天记录会被完全覆盖,仅保留最新的用户提问与 AI 回复。这是 Streamlit 开发聊天应用的高频典型坑点。验证方式:在 Python 代码文件顶部添加打印语句 print("重新执行此文件,渲染展示页面"),通过终端输出可直观观测页面每次交互后是否发生重执行。
问题根因:页面自动重执行
Streamlit 的默认行为为——输入框提交内容、点击发送按钮、鼠标焦点移出输入框时,整个 Python 脚本会从上到下重新执行一遍,触发页面全量重新渲染。页面重执行时原有内存中的临时变量会被重置,导致之前的聊天数据全部丢失,这是消息被覆盖的根本原因。普通局部/全局变量会随页面重执行被重置,无法实现跨运行的数据共享。
Session State 核心机制
Streamlit 官方提供的跨运行共享变量机制,专门用于解决多次页面重执行之间的数据持久化与共享问题,是开发有状态应用(如聊天、表单分步提交)的必考核心 API。采用键值对 key-value 的形式存储数据,支持自定义任意键名与对应值。
聊天消息初始化与存储规则
判断 Session State 中是否存在指定的聊天消息键,若不存在则初始化为空列表,作为存储全量聊天记录的容器。该代码需放置在页面脚本靠前位置,保证每次页面重执行时先完成状态初始化。
# 初始化聊天消息列表(置于脚本靠前位置)
if "messages" not in st.session_state:
st.session_state.messages = []
获取用户输入的提示词后,以 {"role": "user", "content": 用户输入内容} 的格式将消息追加到 st.session_state.messages 列表中。列表中每一条元素都是字典类型,包含两个固定字段:role 标识消息角色,content 标识消息具体内容。role 字段仅允许两个取值:user 代表用户发送的提示词,assistant 代表 AI 大模型返回的响应,该规范是后续聊天组件正确渲染的前提。
重点速览与拍图要点
- 页面重执行机制(输入框提交、焦点移出触发全量重执行)是聊天记录被覆盖的根本原因,核心考点。
- Session State 是必考核心 API,需掌握键值对存储与跨运行共享特性。
- 聊天消息数据结构规范:每条消息必须是含 role/content 字段的字典,role 仅可取 user/assistant,是后续组件渲染前提。
- 聊天状态初始化:判断键是否存在,不存在则初始化为空列表,保证容器不被重复清空。
- 拍图总结:Session State 是"不会被页面重启删掉的安全储物柜",避免临时变量被一键清空。
第 19 节 · 滚雪球拼接会话记忆实现
大模型无状态特性
AI 大模型本身不具备原生会话记忆能力,每一次请求和响应都是相互独立的,无法自动关联之前的对话内容。通过"12个苹果3人怎么均分"后追问"那两个人呢"的测试,大模型会反问用户具体指代场景,完全偏离均分苹果的提问初衷。所有会话记忆必须应用层自行实现。
示例请求结构(两次独立请求):
# 第一次请求
"messages": [
{"role": "system", "content": "你是一名可爱的AI助手,你的名字叫小甜甜,请以亲切、可爱语气来回答用户的问题" },
{"role": "user", "content": "12个苹果,3个人怎么均分" }
]
# 第二次独立请求
"messages": [
{"role": "system", "content": "你是一名可爱的AI助手,你的名字叫小甜甜,请以亲切、可爱语气来回答用户的问题" },
{"role": "user", "content": "那2个人呢?" }
]
滚雪球式消息拼接方案
每一次发起新请求时,把之前所有轮次的用户提问、AI 回复的历史消息全部携带给大模型,让大模型基于完整上下文理解新问题。该方案称为"滚雪球"模式,随着对话轮次增加,携带的历史消息总量不断累积,是实现轻量级会话记忆最基础、最通用的方案。消息存储依托之前实现的 st.session_state.messages 列表,所有用户和 AI 的交互消息都已预先存储,无需额外开发存储逻辑。
代码实现核心逻辑
预先存储的消息列表中,每个元素都是符合大模型接口规范的 {"role":xxx,"content":xxx} 格式字典,无需额外做格式转换。使用 Python 列表解包运算符 *,直接将 st.session_state.messages 列表中的所有字典元素逐个展开,追加到请求的 messages 数组中。
# 调用大模型
url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
payload = {
"model": "glm-4-flash",
"messages": [
{
"role": "system",
"content": system_prompt
},
# 记忆功能:解包历史消息列表
*st.session_state.messages
],
"stream": False,
"temperature": 1
}
开发过程中可以通过 print 打印请求的完整 messages 内容,校验历史消息是否正确拼接,避免出现格式错误导致接口调用失败。
功能验证
多轮连续提问"12个苹果3个人怎么均分"、"6个人呢"、"两个人呢",AI 均能基于上下文正确计算出每人分到的苹果数量。控制台日志可见 system 提示词、每一轮用户提问、每一轮 AI 回复都按顺序完整拼接在请求参数中。后续课程将讲解持久化会话管理、新建会话等进阶功能,实现跨页面刷新的会话留存。
重点速览与拍图要点
- 大模型无状态特性:每次请求响应独立,原生不原生支持会话记忆,面试实操常考。
- 滚雪球方案:每次新请求携带全部历史消息,总量随轮次累积。
- * 列表解包运算符:将历史消息列表逐个展开,是代码实现核心技巧。
- 前置数据结构设计重要性:按接口规范存消息,避免后续额外格式转换。
- 拍图总结:技术原理为滚雪球式上下文累积,每次请求将 system/user/assistant 角色内容完整传递;session/用户/assistant 三角色结构(system 定义身份、user 输入、assistant 回答)。
第 20 节 · 流式输出与响应包解析优化
非流式输出的体验痛点
非流式输出需要等待大模型生成完所有内容后,一次性将结果返回前端展示,当生成内容较长时用户等待时间可达数十秒,体验极差。典型场景:提问"从政治、经济、文化三个角度论述法国大革命爆发的原因(1000字左右)",用户需长时间等待无任何反馈。非流式仅满足基础功能,不适合长文本生成场景。
流式输出的交互优势
参考主流 AI 产品(如豆包、ChatGPT)的逻辑,大模型逐字/逐词持续输出内容,用户实时看到响应过程,感知服务正在运行,大幅降低等待焦虑。流式输出是当前 AI 对话应用的标准交互方式,是项目开发的必做优化项。
流式输出基础参数配置
调用大模型对话接口时,仅需将 stream 参数从 False 修改为 True,即可开启流式输出模式。但仅修改 stream=True 无法直接运行,必须同步修改结果解析逻辑,否则程序直接报错。
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": system_prompt},
*st.session_state.messages
],
stream=True
)
流式响应数据结构解析
流式输出不会一次性返回完整结果,而是将内容拆分为多个独立数据包逐个返回,每个数据包仅包含一小段文本(单字、词语或标点)。必须通过调试工具(如 AXBOT)提前确认流式响应的嵌套结构,按照 choices[0].delta.content 的层级逐层提取内容,不能沿用非流式的 choices[0].message.content 解析路径。
{
"id": "…",
"object": "chat.completion.chunk",
"created": 1767870632,
"model": "deepseek-chat",
"system_fingerprint": "…",
"choices": [
{
"index": 0,
"delta": {
"content": "嘻嘻"
},
"logprobs": null,
"finish_reason": null
}
]
}
代码实现与常见错误排查
基础解析逻辑:遍历所有响应数据包,逐个提取 delta.content 中的非空内容,拼接为完整返回结果。错误写法——在循环中反复调用 st.chat_message("assistant") 生成消息框,会导致循环多少次就生成多少条重复消息,完全不符合预期。
full_response = ""
for chunk in response:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
full_response += content
st.chat_message("assistant").write(full_response)
正确实现方案:使用 Streamlit 的空容器方法 st.empty(),在循环外预先创建一个空的消息占位容器,循环内仅更新该容器的内容,全程只生成一条消息,实现逐字流式输出的效果。
效果验证
先后发送"你好"、"12个苹果两个人怎么分"等测试问题,验证实现了单条消息逐字输出的流畅效果,无重复消息问题,用户体验达到主流 AI 产品标准。
重点速览与拍图要点
- 仅修改 stream=True 无法直接运行,必须同步修改结果解析逻辑,否则报错。
- 流式响应解析路径:chunk.choices[0].delta.content,不能沿用非流式 choices[0].message.content。
- 禁止在循环内部反复调用 st.chat_message(),否则生成大量重复消息。
- 非流式用 response.choices[0].message.content 获取完整结果,适用短文本。
- 拍图总结:流式解析流程为初始化 full_response → 遍历 response 中每个 chunk → 提取非空 chunk.choices[0].delta.content → 累加并实时更新 UI。
第 21 节 · 系统提示词与侧边栏定制
核心功能概述
AI 应用的定制功能分为两大块:一是会话管理功能(新建、删除、取消会话),二是 AI 智能伴侣的昵称与性格定制功能。用户输入的伴侣昵称、性格信息会被组装为系统提示词提交给大模型,大模型将严格按照设定的角色规则与用户交互。系统提示词是大模型理解角色设定的唯一依据,所有自定义属性最终都要通过它传递给大模型。
AI 伴侣系统提示词设计
提示词包含 7 项核心规则:每次仅回复一条消息、禁止场景/状态描述、匹配用户聊天习惯、回复节奏贴近微信日常聊天、可适当使用表情、符合指定性格、内容充分体现性格特征。系统提示词需要在开发过程中不断打磨调试,无法一次性编写完美,需根据大模型回复效果持续优化调整,并需明确指定伴侣的初始昵称与性格作为默认运行参数。
Streamlit 侧边栏两种实现方式
元素直接指定法:通过 st.sidebar.元素名 的形式,直接在侧边栏中添加各类 UI 组件。若要将元素渲染在侧边栏中,必须在组件调用前加上 st.sidebar. 前缀,否则元素会默认显示在主界面。
上下文管理器法:使用 with st.sidebar: 上下文块,块内所有组件无需额外加前缀,会自动渲染到侧边栏,是官方推荐的更便捷写法。
# 第一种:st.sidebar. 前缀
st.sidebar.title("伴侣信息")
st.sidebar.text_input("昵称", placeholder="请输入伴侣的昵称")
st.sidebar.text_area("性格", placeholder="请输入伴侣的性格")
# 第二种:上下文管理器(官方推荐)
with st.sidebar:
st.title("伴侣信息")
st.text_input("昵称", placeholder="请输入伴侣的昵称")
st.text_area("性格", placeholder="请输入伴侣的性格")
伴侣信息组件与参数
基础组件:在侧边栏添加"伴侣信息"标题,分别创建单行文本输入框 st.text_input() 用于填写昵称、多行文本域 st.text_area() 用于填写性格描述。通过查看源码参数说明,掌握 placeholder(空值提示)和 value(默认值)两个核心属性:placeholder 仅在输入框为空时显示提示文字,不会作为真实值提交;value 用于设置输入框的初始填充内容。
Session 状态数据共享
页面刷新后输入框数据会丢失,需要将用户输入的昵称、性格数据存储到 st.session_state 中,实现多次页面加载间的数据共享。初始化时为 Session 中的昵称、性格键设置默认值,当用户输入新内容时实时更新 Session 中对应的键值。
动态系统提示词生成
将固定的系统提示词改造为带占位符的模板,使用 %s 标记需要动态替换的昵称和性格位置。调用大模型接口时,从 Session 中读取最新的昵称和性格数据,替换模板中的占位符,生成最终的个性化系统提示词。
# 模板字符串使用 %s 标记昵称、性格占位符
# 调用大模型接口时从 Session 读取最新数据,用 % 运算符替换占位符
system_prompt = template % (st.session_state.name, st.session_state.nature)
全流程验证
修改侧边栏中的昵称和性格为自定义值,向大模型提问"你叫什么名字"、"你是哪里人",验证大模型是否严格按照自定义设定回复。验证环节是确保动态提示词正确传递、大模型角色生效的关键步骤。
重点速览与拍图要点
- 侧边栏元素渲染规则:必须加 st.sidebar. 前缀或使用上下文管理器,否则组件渲染到主界面,高频易错点。
- placeholder 与 value 差异:placeholder 仅为空提示不参与真实值提交;value 是输入框真实初始值,常考区分。
- 动态提示词生成逻辑:必须从 Session 读取最新用户自定义数据,替换模板占位符,才能让大模型使用最新角色设定。
- 拍图总结:侧边栏含 AI 控制面板(名字、性格、当前会话标识)与历史会话管理两大区域。
第 22 节 · 会话管理四大功能与JSON持久化
会话管理功能总览
会话管理是 AI 智能伴侣侧边栏的第二阶段开发任务,包含新建会话、保存会话、加载历史会话、删除历史会话四项功能,完全对齐主流 AI 对话产品的交互逻辑。点击新建会话按钮可开启全新对话,点击历史会话条目可恢复对应对话上下文,点击条目旁的叉号按钮可删除指定历史会话。这类产品的会话数据不会因应用关闭或设备重启丢失,是功能实现的核心参考标准。
新建会话执行流程
点击新建会话按钮时,首先将当前正在进行的会话数据完整保存,之后创建全新的空白会话并完成持久化存储。绝对不能直接清空当前会话数据,必须先完成旧会话的持久化操作,否则会出现用户对话内容丢失的问题。流程为:点击新建会话按钮 → 保存当前会话数据到磁盘文件 → 创建新会话并保存新会话数据到磁盘文件。
持久化方案选型
如果将会话数据存储在内存的 session 中,一旦服务重启、应用关闭或设备断电,所有会话数据会直接永久丢失,完全无法满足长期保存需求。磁盘文件持久化方案将会话数据写入磁盘文件进行永久存储,彻底规避内存数据易失问题,即使应用关闭、设备重启,依然可以从磁盘文件读取恢复所有会话内容。内存中存放的数据在计算机关机后就会消失,要永久保存数据,就需要将数据保存在文件中。
唯一文件名生成规则
采用会话创建的时间戳作为磁盘文件的名称,格式为年月日_时分秒.json,例如 20260109_160520.json,天然保证文件名全局唯一,同时可以直接作为会话的唯一标识。
会话文件核心数据项
每个历史会话的 JSON 文件中必须保存四类信息,缺一不可:1. 交互消息(用户与 AI 的全部对话记录);2. 伴侣昵称;3. 伴侣性格设定;4. 唯一会话标识。不同会话可以独立定制 AI 伴侣的昵称和性格,这部分个性化配置必须与会话对话数据绑定存储,加载历史会话时才能完整恢复当时的 AI 角色设定。
{
"messages": [
{
"role": "user",
"content": "你好"
},
{
"role": "assistant",
"content": "哈囉~今天過得怎麼樣呀? ❤️"
}
],
"nick_name": "小美",
"nature": "温柔可爱的台湾腔姑娘",
"current_session": "2026-01-09_16-05-20"
}
选型优势与后续规划
JSON 格式结构清晰直观、可读性强,非常适合存储结构化会话数据,Python 也提供了成熟的标准库对 JSON 文件进行读写操作。下一小节将讲解 Python 中操作 JSON 文件的具体实现方法,包括如何将会话数据写入磁盘文件、如何从磁盘文件读取加载历史会话数据。
重点速览与拍图要点
- 内存存储致命缺陷:session 数据在服务重启、应用关闭或设备断电后永久丢失。
- 持久化核心要求:必须将数据写入磁盘文件,规避内存易失性。
- 会话文件必填字段:交互消息、伴侣昵称、伴侣性格、唯一会话标识,缺一不可。
- 新建会话执行顺序:先保存当前会话数据,再创建新会话,不可颠倒。
- 拍图总结:会话管理机制为保存当前会话 → 生成 JSON 文件;创建新会话并保存 → 生成新 JSON 文件;历史会话列表显示所有保存的文件;命名采用 YYYYMMDD_HHMMSS.json 时间戳格式。
第 23 节 · Python文件打开读写关闭基础
文件操作通用三步逻辑
手动操作磁盘文件时,无论读还是写,都遵循「打开文件 → 执行读/写操作 → 关闭文件」的固定流程。Python 中的文件操作完全复用该逻辑:读文件流程为打开→读取→关闭,写文件流程为打开→写入→关闭。三步流程是所有文件读写代码的基础框架,不能省略任何一步。
open() 内置函数详解
open() 是 Python 提供的内置文件打开函数,常用三个核心参数:文件路径(指定目标文件位置)、操作模式(r 代表读取 read,w 代表写入 write)、编码格式(指定文件字符编码)。使用 w 模式打开文件时,如果目标文件不存在,Python 会自动创建该文件;如果文件已存在,会直接覆盖原有内容,不会报错。
# 读文件打开示例
f = open("resources/望庐山瀑布.txt", "r", encoding="utf-8")
# 写文件打开示例
f = open("resources/静夜思.txt", "w", encoding="utf-8")
字符编码核心概念
编码是将字符(文字、数字、符号)转换为计算机可识别的二进制数字代码的规则系统。常见编码对比:ASCII 仅支持数字、字母和基础符号,无法处理中文;GBK 是中国标准编码,支持中文但不兼容韩文、日文等其他语言;UTF-8 全球通用编码,兼容所有语言字符并向下兼容 ASCII,是项目开发首选。操作包含中文的文件时,必须指定 encoding="utf-8",否则极易出现乱码。
文件读取三种常用方法
read():无参数调用时一次性读取文件全部内容,返回完整字符串。readline():逐行读取,每次调用返回一行字符串。readlines():一次性读取所有行,返回列表,每个元素对应文件的一行字符串。使用 readlines() 读取时,每行字符串末尾会自带原始文件中的换行符 \\n,搭配 print 自带的换行效果会出现多余空行,可通过 strip() 方法去除字符串首尾的空白字符(包括换行符)解决。
# 完整读文件示例
f = open("resources/望庐山瀑布.txt", "r", encoding="utf-8")
content = f.read()
print(content)
f.close()
文件写入基础操作
通过文件对象调用 write() 方法向文件写入指定字符串,支持通过 \\n 实现换行。连续多次调用 write() 即可写入多行内容,添加两个连续的 \\n 可在两行内容之间插入一个空行。
# 完整写文件示例
f = open("resources/静夜思.txt", "w", encoding="utf-8")
f.write("窗前明月光,\\n")
f.write("疑是地上霜。\\n")
f.write("举头望明月,\\n")
f.write("低头思故乡。\\n")
f.close()
文件关闭必要性
调用文件对象的 close() 方法可以释放 Python 对该文件的占用权限。如果操作完文件后未调用 close() 关闭,且程序仍在运行,该文件会一直被 Python 程序独占,其他程序或用户无法对其进行修改、删除等任何操作。
重点速览与拍图要点
- 文件操作三步流程:打开 → 读/写 → 关闭,是文件操作题基础框架。
- w 模式特性:文件不存在则自动创建,已存在则直接覆盖原有内容。
- UTF-8 必要性:操作含中文文件必须指定 encoding="utf-8",否则乱码。
- readlines() 换行问题:每行自带 \\n,搭配 print 默认换行产生多余空行,需用 strip() 处理。
- close() 必要性:不调用会导致文件被程序长期占用,其他程序无法操作。
- 拍图总结:编码是字符到二进制 0/1 的转换规则;文件三要素为 open()(建立连接)、read()/write()(读写)、close()(释放资源)。
第 24 节 · try finally与with资源管理
文件操作异常风险场景
常规三步式文件操作(打开-读写-关闭)中,若读写代码抛出异常,后续的 close() 语句将无法执行,文件会持续被占用,其他程序无法正常访问该文件。文件资源泄漏是 Python 文件开发中的典型隐患,必须通过语法机制保障关闭逻辑 100% 执行。
方案一:try…finally 手动资源释放
将文件读写的业务代码放入 try 代码块,把文件关闭的资源释放代码放入 finally 代码块,利用 finally 块无论程序是否抛出异常都会执行的特性,保障文件一定能被关闭。try 代码块不能单独使用,必须搭配 except 或 finally 关键字,此处无需额外捕获异常时可直接使用 try…finally 组合。
# 1. 打开文件
f = open("resources/静夜思.txt", "w", encoding="utf-8")
try:
# 2. 写入文件
f.write("静夜思(李白)\\n")
f.write("窗前明月光, \\n")
f.write("疑是地上霜。\\n")
f.write("举头望明月, \\n")
f.write("低头思故乡。\\n")
finally:
# 3. 关闭文件
f.close()
该方案逻辑清晰但代码结构繁琐,需要手动编写关闭逻辑,开发效率较低。可主动构造 1/0 这类运行时异常验证:即使程序报错,finally 块中的 f.close() 依然会正常执行,文件资源不会泄漏。
方案二:with 上下文管理器推荐方案
with 是 Python 内置的上下文管理器,核心作用是确保资源总是被正确获取和释放,代码块执行完毕后会自动关闭文件,即使代码块内出现异常也能正常释放资源。with open() 是 Python 项目开发中文件操作资源释放的官方推荐最佳实践,无需手动编写 close() 和异常处理代码。
# 写文件
# 1. 打开文件
with open("resources/静夜思.txt", "w", encoding="utf-8") as f:
# 2. 写入文件内容
f.write("静夜思(李白)\\n\\n")
f.write("窗前明月光, \\n")
f.write("疑是地上霜。\\n")
f.write("举头望明月, \\n")
f.write("低头思故乡。\\n")
as f 将 open() 函数返回的文件对象赋值给变量 f,后续直接通过 f 完成读写操作,代码结构简洁清晰。with 方案自动管理资源、代码简洁,完全避免手动编写资源释放代码的繁琐。
两种方案对比与总结
try…finally 方案逻辑直观但代码冗余;with 方案自动管理资源、代码简洁,减少冗余代码同时降低出错概率。实际开发优先选择 with 语句实现文件资源释放。文件资源释放共有两种实现方式:try…finally 手动保障关闭,以及 with open() 自动管理资源,后者是项目开发推荐最佳实践,必须牢记避免文件句柄泄漏。
重点速览与拍图要点
- 文件异常资源泄漏:读写代码抛异常导致 close() 无法执行,文件句柄泄漏。
- try…finally 机制:利用 finally 块无论是否异常都执行,保障关闭逻辑 100% 运行。
- with 上下文管理器:代码块结束自动关闭文件,异常场景也能正常释放资源。
- 开发最佳实践:必须优先使用 with open(),避免手动资源管理冗余与出错风险。
- 拍图总结:核心结论为 with open() 是最佳实践,确保资源使用后总被正确释放(即使异常),优于 try…finally 繁琐实现。
第 25 节 · JSON 模块序列化与反序列化
知识精讲
AI 智能伴侣会话存储需求
AI 智能伴侣的会话文件需要保存四类核心信息:交互消息列表 messages、伴侣昵称 nick_name、伴侣性格 nature、当前会话标识 current_session。使用 JSON 格式存储可以让文件结构清晰,方便后续读写解析。JSON 是软件开发中通用的数据交换格式,非常适合结构化存储这类键值对形式的会话数据。
示例 JSON 结构:
{
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "哈囉~今天過得怎麼樣呀? ❤️"}
],
"nick_name": "小美",
"nature": "温柔可爱的台湾腔姑娘",
"current_session": "2026-01-09_16-05-20"
}
Python json 模块核心概念
Python 标准库内置了 json 模块,无需额外安装,专门用于简化 JSON 格式数据的处理操作。
- 序列化操作:将 Python 对象(如字典)转换为 JSON 格式字符串并写入文件,使用 json.dump() 函数实现。
- 反序列化操作:从 JSON 文件中读取数据,将 JSON 格式字符串还原为 Python 对象,使用 json.load() 函数实现。
序列化和反序列化是 json 模块最核心的两个操作,也是后续项目开发中高频使用的功能。
序列化写入 JSON 文件的基础实现
基础代码流程:导入 json 模块,构造 Python 字典对象,使用 with open() 以写入模式打开文件,调用 json.dump() 将对象写入文件。打开文件时必须指定 encoding="utf-8",避免中文内容出现乱码。
import json
obj = {
"name": "涛哥",
"age": 18,
"gender": "男",
"hobbies": ["reading", "swimming"]
}
with open("resources/session.json", "w", encoding="utf-8") as f:
json.dump(obj, f)
序列化关键参数详解
- ensure_ascii 参数:默认值为 True,会对所有非 ASCII 字符(如中文)进行转义处理,导致文件中中文显示为 Unicode 编码;将其设置为 False 即可保留中文原样输出。
- indent 参数:用于指定 JSON 数据的缩进空格数,设置为 2 时会让每个键值对单独占一行并缩进 2 个空格,大幅提升 JSON 文件的可读性。
只要 JSON 数据中包含中文,就必须手动设置 ensure_ascii=False,这是最常见的易错点。
import json
user = {
"name": "涛哥",
"age": 18,
"gender": "男",
"hobbies": ["reading", "swimming"]
}
with open("resources/user.json", "w", encoding="utf-8") as f:
json.dump(user, f, ensure_ascii=False, indent=2)
反序列化读取 JSON 文件
使用 with open() 以只读模式打开 JSON 文件,调用 json.load() 直接读取文件内容,自动将 JSON 字符串转换为 Python 字典对象。json.load() 返回的结果是原生 Python 字典,可以直接通过键名取值,不需要额外做类型转换。
import json
with open("resources/session.json", "r", encoding="utf-8") as f:
obj = json.load(f)
print(obj)
print(type(obj))
json 模块操作小结
- 序列化:json.dump(对象, 文件句柄, ensure_ascii=False, indent=2),完成 Python 对象到 JSON 文件的持久化存储。
- 反序列化:json.load(文件句柄),完成 JSON 文件数据到 Python 字典的还原读取。
重点速览
- 考点重点
- ensure_ascii 参数配置:当 JSON 数据中包含中文时,必须将 json.dump() 的 ensure_ascii 参数设置为 False,否则中文会被转义为 Unicode 编码,无法正常显示。
- indent 参数作用:设置 indent 参数可以格式化 JSON 文件,添加指定数量的缩进空格,提升文件可读性,是日常开发中常用的优化配置。
- 反序列化返回值类型:json.load() 读取 JSON 文件后返回的是 Python 原生字典对象,不需要额外做类型转换,可以直接通过键名取值。
- 核心概念
- 序列化:将 Python 对象转换为 JSON 格式字符串并写入文件的持久化过程。
- 反序列化:从 JSON 文件中读取数据,将 JSON 字符串还原为 Python 对象的过程。
- json.dump():Python 标准库 json 模块提供的序列化函数,接收 Python 对象和文件句柄作为必填参数。
- json.load():Python 标准库 json 模块提供的反序列化函数,接收已打开的 JSON 文件句柄作为参数。
拍图总结
AI 智能伴侣-会话管理应用场景:支持新建会话、历史会话管理(含删除操作)及个性化设置(如名称“小美”、性格“温柔可爱的台湾腔姑娘”)。会话数据以 JSON 文件形式保存,包含消息列表 messages、用户昵称 nick_name、性格描述 nature 及当前会话标识 current_session。
Python 读写 JSON 文件实战
写入 JSON 文件(序列化):
import json
user = {
"name": "张三",
"age": 18,
"gender": "男",
"hobbies": ["reading", "swimming"]
}
with open("resources/session.json", "w", encoding="utf-8") as f:
json.dump(user, f, ensure_ascii=False, indent=2) # ensure_ascii=False保留中文,indent=2格式化缩进
读取 JSON 文件(反序列化):
import json
with open("resources/session.json", "r", encoding="utf-8") as f:
obj = json.load(f) # 解析JSON文件内容为Python字典
print(obj) # 输出:{'name': '张三', 'age': 18, 'gender': '男', 'hobbies': ['reading', 'swimming']}
关键方法总结
| json.dump() | 将 Python 对象序列化并写入 JSON 文件 | 保存 AI 会话数据到本地文件 |
| json.load() | 从 JSON 文件读取数据并反序列化为 Python 对象 | 加载历史会话记录到应用程序 |
第 26 节 · 会话标识生成与按钮组件
知识精讲
AI 智能伴侣会话管理核心需求回顾
会话管理核心流程:点击新建会话时,首先持久化保存当前正在进行的会话数据,再初始化一个全新的空白会话,保证历史交互信息不丢失。
会话持久化存储方案:采用 JSON 格式存储会话数据,优势为语法简洁、结构清晰,便于后续程序读写操作。需要持久化的核心数据包含:与 AI 大模型的交互消息列表、AI 智能伴侣的自定义昵称、AI 智能伴侣的性格设定、唯一会话标识。会话标识必须保证全局唯一,课程中采用会话创建的时间作为标识,避免出现会话文件重名覆盖的问题。
Streamlit Session State 会话状态共享机制
核心作用:Streamlit 页面每次交互后会自动重新运行全量代码,存储在 session_state 中的数据可以在多次页面重运行之间实现跨轮次共享,不会被重置清空。
会话标识初始化逻辑:页面加载时先判断 session_state 中是否已存在当前会话标识,若不存在则自动生成新的唯一会话标识并存入 session_state 中。
Python datetime 模块时间获取与格式化
模块导入两种写法:第一种是直接导入完整 datetime 模块,第二种是从 datetime 模块中单独导入 datetime 类,简化后续代码书写。
时间格式化规则:通过 strftime() 方法将 datetime 对象转换为自定义格式的字符串,不同占位符对应不同时间维度。必须牢记常用格式化占位符含义,避免时间格式配置错误导致会话标识重复。
from datetime import datetime
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
运行上述代码后控制台将输出形如 2026-01-11 17:29:34 的标准格式化时间字符串,自动省略默认 datetime 对象自带的毫秒部分。
Streamlit st.button 按钮组件开发
- 基础调用规则:st.button() 唯一必填参数为按钮显示的文本内容,支持配置图标、宽度等可选参数。
- 返回值特性:st.button() 返回布尔值,按钮被点击时返回 True,未被点击时返回 False,可通过 if 条件分支绑定点击后的执行逻辑。
- 按钮样式优化:通过设置 use_container_width=True 参数,让按钮宽度自动铺满父容器(侧边栏),实现更美观的页面布局效果。
按钮点击触发的逻辑仅在本次页面重运行中生效,后续页面刷新后返回值会自动重置为 False,不能直接依赖按钮返回值做跨轮次状态持久化。
会话数据持久化存储实现
会话数据封装:将 session_state 中存储的交互消息、昵称、性格、会话标识全部提取出来,封装为 Python 字典对象,作为待持久化的会话数据载体。
会话存储目录创建:使用 os 模块的 path.exists() 判断存储会话文件的 session 目录是否存在,若不存在则自动创建该目录,避免后续写入文件时出现路径不存在的报错。必须提前做目录存在性判断,否则程序运行时会抛出 FileNotFoundError 异常。
JSON 文件写入操作:使用 Python 内置 json 模块的 dump() 方法,将封装好的会话字典写入以会话标识命名的 .json 文件中,指定 utf-8 字符集避免中文乱码,同时配置 indent 参数实现 JSON 文件格式化缩进,提升文件可读性。
通用保存会话函数封装
将保存会话的完整逻辑抽取为独立的 save_session() 函数,实现代码复用,后续项目中任意位置需要保存会话数据时,直接调用该函数即可,避免重复编写冗余代码,同时保证侧边栏页面代码结构清晰,不会被存储逻辑打断。
重点速览
- 考点重点
- Streamlit session_state 特性:跨页面重运行数据共享,是实现多轮同一会话的核心基础。
- strftime 格式化占位符:必须牢记 %Y、%m、%d、%H、%M、%S 的对应含义,是 Python 时间处理的基础考点。
- st.button 返回值特性:按钮点击仅在本次重运行返回 True,刷新后自动重置,是 Streamlit 交互开发的常见易错点。
- 目录存在性判断:写入文件前必须先判断存储目录是否存在,否则会抛出 FileNotFoundError 异常,是文件操作的高频坑点。
- 核心概念
- 会话标识:使用格式化后的当前时间作为唯一标识,保证每个会话对应唯一的存储文件,避免文件重名覆盖。
- JSON 持久化:将会话字典通过 json.dump() 写入文件,兼顾可读性与程序解析便捷性,是本项目的核心存储方案。
- 通用 save_session 函数:封装完整的会话保存逻辑,实现代码复用,降低项目冗余度,提升代码可维护性。
时间格式化核心代码:
from datetime import datetime
datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
拍图总结
Python 日期时间处理基础:通过 datetime 模块获取并格式化当前时间。使用 from datetime import datetime 语句导入 datetime 类。
# 获取当前时间并格式化输出
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
datetime.now():返回当前本地时间的 datetime 对象。strftime(format):将 datetime 对象按指定格式转换为字符串,常用格式符:%Y 4 位年份、%m 2 位月份、%d 2 位日期、%H 24 小时制小时、%M 分钟、%S 秒。执行代码后控制台输出:2026-01-11 17:29:34(格式为年-月-日 时:分:秒)。
第 27 节 · 新建会话功能实现
知识精讲
新建会话功能的核心设计思路
会话数据存储基础:当前所有绘画交互数据、伴侣昵称性格、会话名称均存储在 session_state 中,新建会话时需要针对性重置相关数据。
新建会话的核心两步逻辑:第一步保存当前会话数据,第二步创建全新会话,二者缺一不可。
新建会话的字段重置规则:
- 必须将 session_state 中的 message 消息列表重置为空列表,避免新会话继承上一次的交互记录。
- 伴侣昵称和性格字段无需强制重置,可沿用之前的配置。
- 必须生成全新的会话标识,基于点击新建按钮的当前系统时间构建会话名称。
代码重构优化:抽取公共函数
将重复的时间格式化生成会话名的代码抽取为独立函数 generate_session,两处调用点直接复用该函数,消除代码冗余。该函数设置返回值,将格式化后的时间字符串直接返回,供两处需要生成会话标识的代码位置直接调用。
会话数据持久化实现
自动创建会话目录:代码中加入目录判断逻辑,当 sessions 目录不存在时会自动创建,无需开发者手动提前建立目录。
新建会话的持久化逻辑:完成新会话的字段重置后,调用保存会话的方法,将空消息列表的新会话对象写入独立的持久化文件中。点击一次新建会话按钮,最终会在 sessions 目录下生成两份持久化文件:一份是之前交互完成的旧会话文件,一份是刚创建的空新会话文件。
页面渲染异常问题排查与解决
页面执行顺序的核心特性:Streamlit 框架中点击按钮后,会先完整重新渲染整个页面脚本,执行完所有页面渲染逻辑后,才会执行按钮绑定的自定义处理逻辑。这是最容易踩坑的执行顺序,直接导致消息列表清空后页面无法同步刷新。
手动触发页面重运行方案:调用 Streamlit 提供的 st.rerun() 方法,手动让页面重新执行一次,此时已经被置空的消息列表就会被正确渲染到页面上,展示全新的空白会话界面。
空会话防重复创建优化
异常现象识别:未优化前连续多次点击新建会话按钮,会生成大量无任何交互数据的空会话文件,这不符合主流 AI 对话产品的交互逻辑。
空会话拦截判断逻辑:在新建会话的核心逻辑外层增加条件判断,仅当当前会话的 message 消息列表非空时,才执行保存旧会话、创建新会话的完整流程。
Python 中空列表会自动转换为布尔值 False,非空列表转换为 True,可以直接作为判断条件,简化代码写法。例如直接编写 if st.session_state.messages: 即可作为判断条件。
功能最终验证与后续规划
优化后,在空白会话状态下重复点击新建按钮,不会生成多余的空会话文件,仅当存在有效交互记录时才会创建新会话,完全符合预期交互效果。下一小节将实现历史会话信息的列表展示功能。
重点速览
- 考点重点
- 新建会话两步核心逻辑:点击一次新建按钮,必须先保存当前旧会话,再创建新会话,最终生成两份持久化文件。
- Streamlit 按钮执行顺序:点击按钮后先完整渲染整个页面,再执行按钮绑定的自定义逻辑,这是页面不刷新问题的核心根源。
- 空列表布尔特性:Python 中空列表自动转换为 False,非空列表转换为 True,可以直接作为判断条件简化代码。
- 核心概念
- 会话持久化:将会话的所有交互数据以文件形式保存在本地 sessions 目录下,实现跨页面重启的数据留存。
- Streamlit 重运行机制:调用 st.rerun() 方法可以手动触发页面重新执行,将最新的状态数据同步渲染到页面上。
- 空会话拦截:通过判断消息列表是否为空,避免在空白会话状态下重复生成无意义的空会话文件。
拍图总结
新建会话功能核心:会话字段重置规则(消息列表置空、昵称性格可选保留、生成全新会话标识)、页面重运行方法(st.rerun())、空会话拦截判断逻辑(利用空列表布尔特性)。
第 28 节 · 历史会话列表展示
知识精讲
历史会话列表功能整体需求梳理
三大核心功能定义:历史会话模块共包含 3 项功能,分别是展示全部历史会话条目、点击指定条目加载对应会话内容、点击删除按钮移除指定历史会话。
条目数量规则:历史会话列表的条目数量完全由 sessions 目录下的 JSON 会话文件数量决定,每一个文件对应列表中的一个可交互条目。
load_sessions 函数核心实现
该函数用于扫描指定目录,提取所有合法的历史会话名称并返回列表。必须先判断目录是否存在,避免因目录缺失触发程序异常,这是代码健壮性的基础保障。
# 加载所有的会话列表信息
def load_sessions():
session_list = []
# 加载sessions目录下的文件
if os.path.exists("sessions"):
file_list = os.listdir("sessions")
for filename in file_list:
if filename.endswith(".json"):
session_list.append(filename[:-5])
return session_list
利用字符串切片 filename[:-5] 直接截取文件名,自动去掉末尾 5 位的 .json 后缀,得到纯净的会话名称。无论 sessions 目录是否存在,函数都必须返回列表;如果目录不存在,直接返回空列表即可,保证函数返回值类型统一。
IDE AI 辅助编码开关配置
可以通过关闭右下角的“云端模型自动触发”开关,禁用代码自动提示,手动练习 API 编写;需要提示时按下快捷键 Alt + P 即可手动唤起 AI 代码补全。
Streamlit 多列布局 st.columns
布局方法作用:将页面的一行空间拆分为多个等宽或按比例分配的列,实现多个组件在同一行并排展示。
传入整数参数代表将行平均分为 N 列;传入列表参数则按列表数值的比例分配宽度,例如 [4,1] 代表将行空间按 4:1 的比例拆分为两列。
上下文管理器用法:通过解包变量接收列对象,在对应列的上下文代码块中编写组件,即可将组件渲染到指定列中。例如 col1, col2 = st.columns([4,1])。
Streamlit 组件唯一标识 Key 机制
报错原因解析:循环生成多个组件时,如果没有手动指定唯一的 key 参数,Streamlit 会默认使用组件的 label 作为唯一标识;如果多个组件的 label 完全相同,就会触发“重复元素 ID”的报错。
动态生成的批量组件必须手动传入互不重复的 key 参数,比如使用会话名称作为 key,即可保证所有组件标识唯一。为循环生成的删除按钮传入基于会话名称生成的唯一 key,可彻底解决标识冲突问题。
AI 辅助调试实战技巧
遇到报错时,将报错截图、错误信息和当前项目文件上下文一起提交给 AI 助手,让 AI 结合项目代码定位问题,快速生成原因分析和修复方案,大幅提升开发效率。
重点速览
- 考点重点
- 目录存在性判断:读取目录文件前必须先判断目录是否存在,是代码健壮性的基础要求,避免程序意外崩溃。
- 字符串切片截取文件名:使用 filename[:-5] 快速去掉 .json 后缀,是 Python 字符串处理的高频考点。
- st.columns 布局参数规则:整数参数代表平均拆分,列表参数代表按比例分配宽度,是 Streamlit 布局的核心考点。
- 组件唯一 Key 机制:动态批量生成组件时必须手动传入唯一 key,否则会触发重复 ID 报错,是 Streamlit 开发的高频易错点。
- 核心概念
- load_sessions 函数:扫描 sessions 目录,筛选所有 JSON 会话文件,提取会话名称并返回列表的工具函数。
- st.columns 布局方法:Streamlit 提供的多列布局容器,支持将页面一行拆分为多个按比例分配的列,实现组件并排展示。
- 组件唯一 Key:Streamlit 中用于区分不同组件的唯一标识,未手动指定时默认使用组件的 label 属性作为标识。
拍图总结
会话数据存储(JSON 文件):使用 json.dump() 将会话数据写入文件,设置 ensure_ascii=False 保留非 ASCII 字符,indent 参数美化格式。
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)
会话列表加载函数(load_sessions()):初始化空列表 session_list;通过 os.path.exists("sessions") 判断目录是否存在;使用 os.listdir("sessions") 获取文件,遍历并用 filename.endswith(".json") 筛选,提取文件名 filename[:-5];返回会话名称列表。
第 29 节 · 加载指定会话与高亮
知识精讲
AI 智能伴侣会话管理模块总览
三大核心功能:AI 智能伴侣的会话管理包含 3 个核心能力:展示历史会话列表、加载指定历史会话、删除指定历史会话,所有会话数据以独立 JSON 文件的形式存储在 sessions 目录下。三个功能是会话管理的完整闭环,开发时要注意功能之间的数据联动。
目录结构示例:
sessions/
├─ 2026-01-11_18-00-05.json
├─ 2026-01-11_18-04-42.json
├─ 2026-01-11_18-08-56.json
├─ 2026-01-11_18-14-25.json
└─ 2026-01-11_18-45-08.json
加载指定会话的函数封装
将加载指定会话的逻辑独立封装为 load_session(session_name) 函数,让主业务代码更聚焦页面渲染,提升代码可读性与可维护性。
使用 os.path.exists() 判断目标会话 JSON 文件是否存在,避免直接读取不存在的文件抛出异常。文件路径拼接时要注意目录 sessions、会话名 session_name 和后缀 .json 的完整组合,不要遗漏任意一部分。
通过 json.load() 将 JSON 文件内容直接转换为 Python 对象,快速获取会话的所有属性。
def load_session(session_name):
session_file_path = f"sessions/{session_name}.json"
if os.path.exists(session_file_path):
with open(session_file_path, "r", encoding="utf-8") as f:
session_obj = json.load(f)
# 将读取到的数据赋值给streamlit的会话状态
st.session_state.messages = session_obj["messages"]
st.session_state.make_name = session_obj["name"]
st.session_state.nature = session_obj["meta"]
st.session_state.current_session = session_obj
异常处理与用户提示
使用 try-except 包裹文件读取与会话状态赋值的逻辑,捕获运行时可能出现的各类异常。使用 Streamlit 内置的 st.error() 组件向用户展示友好的错误提示,避免直接输出原始异常信息导致用户无法理解。不要直接使用 s.exception() 展示原始异常,面向普通用户的系统必须提供易懂的业务错误提示。
def load_session(session_name):
session_file_path = f"sessions/{session_name}.json"
if os.path.exists(session_file_path):
try:
with open(session_file_path, "r", encoding="utf-8") as f:
session_obj = json.load(f)
st.session_state.messages = session_obj["messages"]
st.session_state.make_name = session_obj["name"]
st.session_state.nature = session_obj["meta"]
st.session_state.current_session = session_obj
st.success("会话加载成功")
except Exception as e:
st.error("加载会话失败")
会话数据持久化 Bug 修复
问题定位:原代码仅在点击“新建会话”按钮时才保存会话数据,用户直接切换历史会话时,当前正在交互的新会话数据没有被持久化到文件中,导致数据丢失。
修复方案:在 AI 大模型完成响应、返回结果之后,立刻调用之前封装好的 save_session() 函数,实时保存当前会话的所有数据。AI 响应完成后立即保存是关键,不要依赖用户手动点击新建会话来触发保存,否则会出现大量数据丢失的情况。
会话按钮高亮交互实现
Streamlit 按钮 type 属性:Streamlit 的按钮组件支持三种 type 属性值:primary(高亮红色)、secondary(默认无高亮)、tertiary,通过修改该属性可以控制按钮的视觉样式。
选中状态判断逻辑:遍历历史会话列表时,判断当前遍历的会话名是否等于 st.session_state.current_session 的会话标识,匹配则设置按钮为高亮样式,否则保持默认样式。
Python 三元运算符语法
标准写法为 <true_value> if 条件表达式 else <false_value>,执行时先判断条件表达式,条件为真返回 true_value,条件为假返回 false_value。三元运算符可以简化简单的二分支条件判断代码,不需要写完整的多行 if-else 结构。
# 控制会话按钮的高亮样式
btn_type = "primary" if session == st.session_state.current_session else "secondary"
重点速览
- 考点重点
- 文件路径拼接校验:加载会话前必须正确拼接 sessions 目录、会话名和 .json 后缀,使用 os.path.exists() 做存在性判断,避免读取异常。
- 用户友好异常提示:面向普通用户的系统禁止直接输出原始异常,必须使用 st.error() 展示易懂的业务错误提示。
- 会话数据保存时机:AI 大模型完成响应后必须立刻调用 save_session() 保存数据,不能依赖用户手动点击新建会话触发保存,否则会出现数据丢失。
- 三元运算符语法:牢记 Python 三元运算符的顺序是“真值在前,条件在中,假值在后”。
- 核心概念
- 加载指定会话函数:封装为 load_session(session_name),传入会话名即可完成对应会话数据的读取与会话状态赋值。
- 三元运算符语法:标准写法为 <true_value> if 条件表达式 else <false_value>。
- 会话状态变量:Streamlit 的 st.session_state 用于全局保存页面的会话数据,修改后调用 st.rerun() 即可触发页面重新渲染。
拍图总结
Python 三元运算符语法:<true_value> if <条件表达式> else <false_value>。当条件表达式为 True 时返回 true_value,为 False 时返回 false_value,适用于简单条件判断,可替代基础 if-else 语句,简化代码结构。
第 30 节 · 删除指定会话功能
知识精讲
删除会话功能的核心需求分析
功能本质拆解:每一条会话对应磁盘上一个独立的 JSON 存储文件,删除会话的核心操作就是删除该会话对应的 JSON 文件,而非仅修改前端展示数据。
交互逻辑要求:点击任意会话条目后的删除按钮,精准删除该条目对应的会话资源,删除完成后需要重新加载会话列表完成界面刷新。不能仅做前端隐藏,必须同步删除磁盘上的持久化文件,否则刷新页面后已删除的会话会重新出现。
delete_session 函数基础框架实现
定义接收会话名称作为入参的删除函数,使用异常捕获机制处理文件操作中的各类错误。代码先通过 os.path.exists() 判断目标会话文件是否真实存在,避免直接删除不存在的文件抛出系统异常。
# 删除会话信息函数
def delete_session(session_name):
try:
if os.path.exists(f"sessions/{session_name}.json"):
os.remove(f"sessions/{session_name}.json") # 删除文件
except Exception:
st.error("删除会话失败!")
删除功能的初测问题排查
初测现象:执行基础删除逻辑后,磁盘上的会话文件已被成功删除,左侧会话列表也同步更新,但右侧当前打开的会话消息列表没有同步清空,出现数据展示不一致的问题。
根因定位:未区分「删除当前正在使用的会话」和「删除其他未激活会话」两种场景,没有针对不同场景做差异化的界面状态更新。
删除会话的边界场景判断逻辑
- 场景 1:删除非当前激活会话:仅需删除对应磁盘文件、刷新左侧会话列表,右侧当前会话的消息展示完全不受影响,保持原有交互状态。
- 场景 2:删除当前正在使用的会话:除了删除磁盘文件之外,必须额外执行两步操作:清空右侧的消息列表、自动生成一个全新的空白会话,让用户可以直接开启新一轮对话。
这是本功能最容易遗漏的边界处理,也是项目开发中的高频易错点,不做该判断会导致已删除会话的残留数据继续展示,引发逻辑混乱。
完整功能的效果验证
- 非当前会话删除测试:打开会话 A,点击会话 B 后的删除按钮,会话 B 被成功移除,会话 A 的消息内容完整保留,交互不受干扰。
- 当前会话删除测试:打开会话 A,点击会话 A 后的删除按钮,会话 A 被移除,右侧自动切换为空白消息界面,系统自动创建新会话,用户可以直接发起新对话,功能闭环完成。
会话管理模块收尾说明
本次删除会话功能开发完成后,新建会话、加载会话列表、加载指定会话、删除指定会话这四项核心会话管理功能全部开发完毕。当前项目仍存在少量待修复的细节 Bug,后续课程将统一进行问题排查与优化。
重点速览
- 考点重点
- 删除前的文件存在性判断:必须先判断目标文件是否存在再执行删除,避免抛出未处理的系统异常,是 Python 文件操作的常规考点。
- 删除会话的双场景判断:区分删除当前激活会话和删除非当前会话的不同处理逻辑,是本功能最核心的易错考点,遗漏该判断会导致严重的展示逻辑错误。
- 核心概念
- 会话持久化机制:每一条会话对应磁盘上独立的 {session_name}.json 文件,会话的增删改查都基于该文件完成,无需依赖重型数据库。
- delete_session 函数核心代码:
def delete_session(session_name):
try:
if os.path.exists(f"sessions/{session_name}.json"):
os.remove(f"sessions/{session_name}.json")
except Exception:
st.error("删除会话失败!")
拍图总结
删除会话函数:定义 delete_session(session_name) 函数,通过 os.path.exists() 检查会话文件是否存在,若存在则调用 os.remove() 删除指定 JSON 文件(路径格式:"sessions/{session_name}.json"),异常时通过 st.error("删除会话失败!") 提示。界面配置包括 st.title("AI智能伴侣") 和 st.logo("resources/logo.png")。
第 31 节 · 全流程测试与技术栈复盘
知识精讲
项目运行与基础功能测试
项目启动方式:在终端中进入对应目录,执行命令 streamlit run 08.py 即可启动 Streamlit 开发的 AI 应用,输入文件名时可按 Tab 键自动补全提升效率。
核心功能验证:依次测试大模型对话交互、会话历史持久化、新建多会话、加载指定会话、自定义伴侣人设、删除会话等功能,确认全部功能运行正常。自定义昵称和性格后,AI 大模型的回复会严格贴合预设人设,验证了系统提示词注入的有效性。
会话列表排序逻辑优化
优化需求:将默认的会话时间正序排序,调整为最新创建的会话显示在最顶部,提升用户操作体验。
实现方案:在 load_sessions 函数获取会话列表后,调用列表内置的 sort() 方法,设置 reverse=True 实现降序倒序排序。
列表的 sort() 方法属于原地排序,返回值为 None,必须先完成排序操作,再将排序后的列表返回,不能直接写 return session_list.sort(reverse=True),否则会返回空值。
def load_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
侧边栏布局细节优化
在侧边栏的会话管理区域和伴侣信息定制区域之间添加分隔线,让功能分区更清晰,避免用户混淆操作区域。直接调用 Streamlit 内置的 st.divider() 方法即可快速生成分隔线,无需额外自定义样式。
with st.sidebar:
st.subheader("AI控制面板")
# 会话管理相关代码省略
st.divider() # 插入功能分隔线
# 伴侣昵称、性格定制相关代码省略
项目四大核心功能梳理
项目全量技术栈梳理
- 大模型部署方案:包含本地部署开源大模型、调用官方开放 API 两种方案,本项目选择调用官方 API 的方式,规避本地硬件性能不足的限制。
- HTTP 协议规范:了解 HTTP 请求、响应的数据格式,以及常见响应状态码的含义,为调用大模型 API 打下基础。
- 大模型交互方案:可通过接口测试工具调试 API,也可基于 OpenAI 兼容的 Python SDK 在代码中完成交互。
- Streamlit 框架使用:快速搭建 Web 应用页面,无需复杂的前端开发,相关 API 无需死记硬背,开发时查阅官方文档按需调用即可。
- Python 文件操作:使用标准库 os 完成文件/文件夹的创建、判断存在、删除等操作,使用 json 库完成 Python 对象与 JSON 格式字符串的互相转换,实现会话数据的持久化存储。
- 时间处理模块:使用 datetime 模块生成带时间戳的会话名称,精准标识每一个会话的创建时间。
项目整体价值总结
AI 智能伴侣是一个综合性 Python 实战项目,将前期学习的 Python 基础语法、第三方库使用、Web 应用开发、大模型对接等知识点进行整合,实现知识的巩固、应用与拓展。
重点速览
- 考点重点
- 列表 sort 方法的返回值易错点:Python 列表的 sort() 是原地排序,返回值为 None,不能直接将 list.sort() 作为函数返回值,否则会返回空值导致程序报错。
- 人设注入的实现逻辑:自定义的昵称和性格信息需要通过字符串格式化注入到系统提示词中,再传递给大模型,才能让 AI 严格按照预设人设回复。
- 删除当前会话的边界处理:删除正在使用的会话时,不能直接清空页面,需要自动生成一个新的空白会话,保证应用可以继续正常交互。
- 核心概念
- 大模型会话记忆:大模型本身无状态不具备记忆能力,通过在请求中持续传入全部历史对话内容,让大模型获取完整上下文,实现记忆效果。
- Streamlit 框架:面向数据与 AI 应用的快速 Web 开发框架,封装了丰富的 UI 组件,无需复杂前端开发即可快速搭建可用的 Web 应用。
- JSON 序列化与反序列化:通过 Python 的 json 库,将内存中的 Python 字典/列表对象转换为 JSON 字符串写入文件,也可以从文件中读取 JSON 字符串还原为 Python 对象,实现数据持久化。
会话倒序排序核心代码:
session_list.sort(reverse=True)
return session_list
拍图总结
AI 智能伴侣项目概述:开发一个具有个性化交互能力的 AI 聊天应用,支持会话管理、角色定制和大模型集成。核心功能包括会话创建/加载/删除、伴侣性格设定、上下文记忆、实时消息交互。技术栈:Python、Streamlit、JSON、datetime、os、requests 库。
核心技术模块
| 页面配置 | st.set_page_config(page_title="AI智能伴侣", layout="wide", initial_sidebar_state="expanded") |
| 会话标识生成 | datetime.now().strftime("%Y%m%d_%H%M%S") |
| 会话存储 | json.dump(session_data, f, ensure_ascii=False, indent=2) |
| 大模型调用 | requests.post(url, json=payload, headers=headers) |
核心函数:save_session()、load_session(session_name)、delete_session(session_name)。
五、知识扩展:Python 文件操作的路径与模式(第 32 节)
第 32 节 · 文件路径写法与 r/w/a 模式
知识精讲
文件操作前置知识回顾
文件操作基础三步法:Python 常规文件操作分为打开文件、读写文件、关闭文件三个核心步骤,企业级开发推荐使用 with 上下文管理器自动管理资源释放,无需手动调用 close() 方法。
with 语句可以自动处理文件关闭逻辑,避免因代码异常导致文件资源泄漏,是工业界的标准写法。
with open("文件路径", "r", encoding="utf-8") as f:
content = f.read()
相对路径的定义与规则
相对路径核心逻辑:相对路径是从当前运行文件所在目录开始查找目标文件的路径写法,无需指定完整的磁盘根目录。
. 代表当前目录,./ 前缀可以省略;.. 代表上一级目录,连续写多个 .. 可以向上回溯多级父目录。
示例:../第2章/file/寻隐者不遇.txt 表示从当前目录退回上一级,再进入第 2 章目录下的 file 文件夹读取目标文件。相对路径写法示例为 ./resources/望庐山瀑布.txt,其中 ./ 可直接省略为 resources/望庐山瀑布.txt。
绝对路径的定义与转义规则
绝对路径核心逻辑:绝对路径是从文件系统根目录开始的完整文件位置路径,Windows 系统从盘符(C 盘/D 盘)开始,Linux/Unix 系统从 / 根目录开始。
Windows 路径中的反斜杠 \\ 在 Python 字符串中是转义字符,直接写会被识别为特殊转义序列(如 \\n 换行、\\t 制表符),必须做转义处理。
两种合法写法:
- 使用双反斜杠转义:D:\\\\Python-Project\\\\py_project01\\\\第3章\\\\resources\\\\望庐山瀑布.txt
- 直接使用正斜杠:D:/Python-Project/py_project01/第3章/resources/望庐山瀑布.txt
可通过右键文件-属性-安全选项卡查看对象完整名称,快速获取文件的绝对路径。
项目开发路径选型最佳实践
相对路径的优势:相对路径的可移植性极强,将整个项目文件夹分享给他人时,无需修改任何路径代码即可直接运行,路径写法简洁清晰,便于团队协作阅读。
绝对路径的弊端:绝对路径绑定了当前设备的磁盘目录结构,更换设备后几乎必然出现路径找不到文件的错误,需要逐行修改路径适配新环境,维护成本极高。
工业级项目开发中强制推荐使用相对路径,避免硬编码绝对路径。
文件操作模式详解
- r 只读模式:以只读方式打开文件,文件指针定位在文件开头,只能执行读取操作,无法写入内容。
- w 写入模式:只写模式,从头编辑文件,如果目标文件已存在,会直接删除原有全部内容再从头写入;如果文件不存在,会自动创建一个新的空文件。
- a 追加模式:追加写入模式,新写入的内容会自动追加到原有内容的末尾,不会覆盖原有文件内容;如果文件不存在,同样会自动创建新文件。
使用 w 模式前必须确认是否需要保留原有文件内容,避免误操作清空重要数据。
# 追加写入古诗示例
with open("resources/静夜思.txt", "a", encoding="utf-8") as f:
f.write("静夜思(李白)\\n\\n")
f.write("窗前明月光,\\n")
f.write("疑是地上霜。\\n")
f.write("举头望明月,\\n")
f.write("低头思故乡。\\n")
代码演示实操验证
- 相对路径回溯演示:在第 3 章目录下的代码文件中,通过 ../ 回溯到上一级目录,成功读取第 2 章目录下的目标文本文件,验证了多级相对路径的正确性。
- 绝对路径转义演示:演示了直接使用单反斜杠路径报错的场景,通过双反斜杠转义或替换为正斜杠,成功解决转义字符冲突问题。
- a 模式追加效果演示:多次运行追加写入代码,目标文件中会不断新增写入的古诗内容,不会清空原有数据,直观展示了追加模式和覆盖模式的核心差异。
重点速览
- 考点重点
- 相对路径符号规则:. 代表当前目录,.. 代表上一级目录,./ 可以省略,是考试中路径编写题的核心考点。
- 绝对路径转义规则:Windows 路径中的单反斜杠必须转义,否则会被识别为转义字符导致报错,是高频易错考点。
- 项目开发路径选型:工业级项目必须使用相对路径,可移植性更强,是实操类题目的标准要求。
- 写入模式差异:w 模式会清空原有文件内容,a 模式仅在末尾追加,使用 w 模式前必须确认是否需要保留原文件,是实操中最容易踩坑的考点。
- 核心概念
- 相对路径:从当前运行文件所在目录开始查找目标文件的路径,无需指定磁盘根目录,可移植性强。
- 绝对路径:从文件系统根目录开始的完整文件位置路径,绑定当前设备的磁盘目录结构,可移植性差。
- 文件操作模式:r 只读、w 覆盖写入、a 追加写入,三种模式的特性是文件操作的核心基础。
- with 上下文管理器:Python 中管理文件资源的标准写法,自动处理文件关闭逻辑,避免资源泄漏。
拍图总结
Python 文件操作基础:使用 with open(路径, 模式, encoding="utf-8") as f: 上下文管理器,自动处理文件关闭,避免资源泄露。
文件路径写法
| 相对路径 | 从当前文件所在目录开始查找 | ./resources/望庐山瀑布.txt(./ 可省略) | 推荐使用,可移植性强、路径简洁 |
| 相对路径 | .. 表示上一级目录 | ../第2章/file/寻隐者不遇.txt | .. 表示上一级目录 |
| 绝对路径 | 从文件系统根目录开始的完整路径 | D:\\\\Python-Project\\\\py_project01\\\\第3章\\\\resources\\\\望庐山瀑布.txt | 包含完整位置信息,移植性差 |
| 绝对路径 | 斜杠 / 需转义为 \\\\ 或直接使用 / | D:/Python-Project/py_project01/第3章/resources/望庐山瀑布.txt | 斜杠 / 无需转义 |
文件操作模式
- r(只读模式):默认模式,文件指针位于开头,文件不存在则报错。
with open("resources/望庐山瀑布.txt", "r", encoding="utf-8") as f:
content = f.read() # 读取全部内容
print(content)
- w(写入模式):覆盖原有内容,文件不存在则创建。
- a(追加模式):在文件末尾添加内容,文件不存在则创建。
with open("resources/静夜思.txt", "a", encoding="utf-8") as f:
f.write("静夜思(李白)\\n\\n")
f.write("窗前明月光,\\n疑是地上霜。\\n举头望明月,\\n低头思故乡。\\n")
开发提示:项目中优先使用相对路径,可避免因环境变化导致路径错误;操作文本文件时需指定 encoding="utf-8",防止中文乱码。
六、项目完整代码(两个版本)
以下两份代码均按课堂原文整理,API Key 已替换为占位符,运行前替换成自己的 Key 即可;./resources/logo.png 需自备。
版本一:AI 智能伴侣最终代码(过程式写法)
特点:从上到下一气呵成,页面配置、函数定义、状态初始化、侧边栏、聊天区全部写在模块层级,跟着写最容易理解,改动也方便。
import streamlit as st
import requests
import os
import json
from datetime import datetime
# 设置页面配置
st.set_page_config(
page_title="AI智能伴侣",
page_icon="🤖",
# 设置页面布局为宽屏
layout="wide",
# 设置侧边栏默认状态为展开
initial_sidebar_state="expanded",
menu_items={
}
)
def generate_session_name():
return datetime.now().strftime("%Y%m%d_%H%M%S")
# 保存会话信息函数
def save_session():
if st.session_state.current_session:
# 构建新的会话对象
session_data = {
"current_session": st.session_state.current_session,
"nick_name": st.session_state.nick_name,
"nature": st.session_state.nature,
"messages": st.session_state.messages
}
# 保存到文件
if not os.path.exists("sessions"):
os.makedirs("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=2)
def new_session():
# st.session_state.nick_name = "小甜甜"
# st.session_state.nature = "活泼开朗的东北姑娘"
if st.session_state.messages:
st.session_state.messages = []
st.session_state.current_session = generate_session_name()
save_session()
st.rerun()
def load_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
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.current_session = session_data["current_session"]
st.session_state.nick_name = session_data["nick_name"]
st.session_state.nature = session_data["nature"]
st.session_state.messages = session_data["messages"]
except Exception as e:
st.error(f"加载会话失败: {e}")
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 = generate_session_name()
except Exception as e:
st.error(f"删除会话失败: {e}")
# 设置页面标题
st.title("AI智能伴侣")
#设置logo
st.logo("./resources/logo.png")
# 设置提示词
system_prompt = """
你叫%s,现在是用户的真实伴侣,请完全代入伴侣角色。:
规则:
1.每次只回1条消息
2.禁止任何场景或状态描述性文字
3.匹配用户的语言
4.回复简短,像微信聊天一样
5.有需要的话可以用❤️🌸等emoji表情
6.用符合伴侣性格的方式对话
7.回复的内容,要充分体现伴侣的性格特征
伴侣性格:
%s
你必须严格遵守上述规则来回复用户。
"""
# 初始化聊天信息
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 = generate_session_name()
# 侧边栏
with st.sidebar:
st.subheader("AI控制面板")
# 新建会话
if st.button("新建会话", width="stretch", icon="✏️"):
# 1.保存当前会话
save_session()
load_sessions()
# 2.新建会话
new_session()
# 历史会话
st.text("历史会话")
session_list = load_sessions()
for session in session_list:
col1, col2 = st.columns([4,1])
with col1:
if st.button(session, width="stretch", icon="📝", key=f"{session}_load",
type="primary" if session == st.session_state.current_session else "secondary"):
load_session(session)
st.rerun()
with col2:
if st.button("", width="stretch",icon="❌️", key=f"{session}_delete"):
delete_session(session)
st.rerun()
st.divider()
# 昵称
nick_name = st.text_input("昵称", placeholder="请输入昵称", value=st.session_state.nick_name)
nature = st.text_area("性格", placeholder="请输入性格", value=st.session_state.nature)
st.session_state.nick_name = nick_name
st.session_state.nature = nature
# 展示聊天信息
st.text("当前会话:" + st.session_state.current_session)
for message in st.session_state.messages:
# if message["role"] == "user":
# st.chat_message("user").write(message["content"])
# else:
# st.chat_message("assistant").write(message["content"])
st.chat_message(message["role"]).write(message["content"])
# 消息输入框
prompt = st.chat_input("请输入您要问的问题")
if prompt:
st.chat_message("user").write(prompt)
print("———-> 调用大模型,提示词:", prompt)
st.session_state.messages.append({"role": "user", "content": prompt})
# 调用大模型
url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
payload = {
"model": "glm-4-flash",
"messages": [
{
"role": "system",
"content": system_prompt % (nick_name, nature)
},
# 记忆功能
*st.session_state.messages
# {
# "role": "user",
# "content": prompt
# }
],
"stream": False,
"temperature": 1
}
headers = {
"Authorization": "Bearer 你的API Key",
"Content-Type": "application/json"
}
# # 非流式输出
# response = requests.post(url, json=payload, headers=headers)
# data = response.json()
# content = data["choices"][0]["message"]["content"]
# st.session_state.messages.append({"role": "assistant", "content": content})
# st.chat_message("assistant").write(content)
# 流式输出
payload["stream"] = True # 1. 请求参数开启流式
response = requests.post(url, json=payload, headers=headers, stream=True) # 2. 边收边读
content = ""
placeholder = st.chat_message("assistant").empty() # 3. 占位容器,用于逐字刷新
for line in response.iter_lines():
if not line: # 跳过空行(SSE 用空行分隔每一帧)
continue
line = line.decode("utf-8")
if not line.startswith("data:"): # 只处理 data: 开头的行
continue
data_str = line[len("data:"):].strip()
if data_str == "[DONE]": # 4. 流结束标志
break
delta = json.loads(data_str)["choices"][0]["delta"]
if delta.get("content"):
content += delta["content"]
placeholder.write(content) # 5. 每来一块就重绘,形成打字机效果
st.session_state.messages.append({"role": "assistant", "content": content})
# 保存会话
save_session()
版本二:AI 重构之后完整代码(函数化拆分)
特点:常量集中配置、按职责拆分为工具函数、会话管理、API 调用、UI 渲染、主函数六块,每个函数带类型标注与文档字符串,可读性、可维护性、可测试性都更好。
import streamlit as st
import requests
import os
import json
from datetime import datetime
# ==================== 常量配置 ====================
PAGE_TITLE = "AI智能伴侣"
PAGE_ICON = "🤖"
SESSIONS_DIR = "sessions"
LOGO_PATH = "./resources/logo.png"
API_URL = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
API_KEY = "Bearer 你的API Key"
MODEL_NAME = "glm-4-flash"
SYSTEM_PROMPT = """你叫%s,现在是用户的真实伴侣,请完全代入伴侣角色。:
规则:
1.每次只回1条消息
2.禁止任何场景或状态描述性文字
3.匹配用户的语言
4.回复简短,像微信聊天一样
5.有需要的话可以用❤️🌸等emoji表情
6.用符合伴侣性格的方式对话
7.回复的内容,要充分体现伴侣的性格特征
伴侣性格:
%s
你必须严格遵守上述规则来回复用户。
"""
# ==================== 工具函数 ====================
def generate_session_name() -> str:
"""生成基于时间戳的会话名"""
return datetime.now().strftime("%Y%m%d_%H%M%S")
def ensure_sessions_dir():
"""确保会话目录存在"""
if not os.path.exists(SESSIONS_DIR):
os.makedirs(SESSIONS_DIR)
def get_session_filepath(session_name: str) -> str:
"""获取会话文件路径"""
return os.path.join(SESSIONS_DIR, f"{session_name}.json")
# ==================== 会话管理 ====================
def save_session():
"""保存当前会话到文件"""
if not st.session_state.current_session:
return
ensure_sessions_dir()
session_data = {
"current_session": st.session_state.current_session,
"nick_name": st.session_state.nick_name,
"nature": st.session_state.nature,
"messages": st.session_state.messages,
}
filepath = get_session_filepath(st.session_state.current_session)
with open(filepath, "w", encoding="utf-8") as f:
json.dump(session_data, f, ensure_ascii=False, indent=2)
def load_sessions() -> list:
"""加载所有历史会话名,按时间倒序"""
if not os.path.exists(SESSIONS_DIR):
return []
session_list = [
f[:-5] for f in os.listdir(SESSIONS_DIR)
if f.endswith(".json")
]
session_list.sort(reverse=True)
return session_list
def load_session(session_name: str):
"""从文件加载指定会话"""
try:
filepath = get_session_filepath(session_name)
if not os.path.exists(filepath):
return
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
st.session_state.current_session = data["current_session"]
st.session_state.nick_name = data["nick_name"]
st.session_state.nature = data["nature"]
st.session_state.messages = data["messages"]
except Exception as e:
st.error(f"加载会话失败: {e}")
def delete_session(session_name: str):
"""删除指定会话"""
try:
filepath = get_session_filepath(session_name)
if os.path.exists(filepath):
os.remove(filepath)
# 如果删除的是当前会话,重置状态
if session_name == st.session_state.current_session:
st.session_state.messages = []
st.session_state.current_session = generate_session_name()
except Exception as e:
st.error(f"删除会话失败: {e}")
def create_new_session():
"""创建新会话"""
save_session() # 先保存当前会话
st.session_state.messages = []
st.session_state.current_session = generate_session_name()
# 重置为默认值
st.session_state.nick_name = "小甜甜"
st.session_state.nature = "活泼开朗的东北姑娘"
save_session()
st.rerun()
# ==================== API 调用 ====================
def call_llm_stream(messages: list) -> str:
"""调用大模型(流式),返回完整回复内容"""
payload = {
"model": MODEL_NAME,
"messages": messages,
"stream": True,
"temperature": 1,
}
headers = {
"Authorization": API_KEY,
"Content-Type": "application/json",
}
response = requests.post(API_URL, json=payload, headers=headers, stream=True)
content = ""
placeholder = st.chat_message("assistant").empty()
for line in response.iter_lines():
if not line:
continue
line = line.decode("utf-8")
if not line.startswith("data:"):
continue
data_str = line[len("data:"):].strip()
if data_str == "[DONE]":
break
try:
delta = json.loads(data_str)["choices"][0]["delta"]
if delta.get("content"):
content += delta["content"]
placeholder.write(content)
except (json.JSONDecodeError, KeyError):
continue
return content
# ==================== 页面初始化 ====================
def init_session_state():
"""初始化 session_state 默认值"""
defaults = {
"messages": [],
"nick_name": "小甜甜",
"nature": "活泼开朗的东北姑娘",
"current_session": generate_session_name(),
}
for key, value in defaults.items():
if key not in st.session_state:
st.session_state[key] = value
# ==================== UI 渲染 ====================
def render_sidebar():
"""渲染侧边栏"""
with st.sidebar:
st.subheader("AI控制面板")
# 新建会话按钮
if st.button("新建会话", width="stretch", icon="✏️"):
create_new_session()
# 历史会话列表
st.text("历史会话")
session_list = load_sessions()
for session in session_list:
col1, col2 = st.columns([4, 1])
with col1:
is_current = (session == st.session_state.current_session)
if st.button(
session,
width="stretch",
icon="📝",
key=f"{session}_load",
type="primary" if is_current else "secondary",
):
load_session(session)
st.rerun()
with col2:
if st.button("", width="stretch", icon="❌️", key=f"{session}_delete"):
delete_session(session)
st.rerun()
st.divider()
# 伴侣设置
st.session_state.nick_name = st.text_input(
"昵称", placeholder="请输入昵称", value=st.session_state.nick_name
)
st.session_state.nature = st.text_area(
"性格", placeholder="请输入性格", value=st.session_state.nature
)
def render_chat_history():
"""渲染聊天历史"""
for message in st.session_state.messages:
st.chat_message(message["role"]).write(message["content"])
def render_chat_input():
"""处理用户输入"""
prompt = st.chat_input("请输入您要问的问题")
if not prompt:
return
# 显示用户消息
st.chat_message("user").write(prompt)
st.session_state.messages.append({"role": "user", "content": prompt})
# 构建消息列表
messages = [
{
"role": "system",
"content": SYSTEM_PROMPT % (st.session_state.nick_name, st.session_state.nature),
},
*st.session_state.messages,
]
# 调用大模型并流式输出
content = call_llm_stream(messages)
# 保存助手回复
st.session_state.messages.append({"role": "assistant", "content": content})
save_session()
# ==================== 主函数 ====================
def main():
# 页面配置
st.set_page_config(
page_title=PAGE_TITLE,
page_icon=PAGE_ICON,
layout="wide",
initial_sidebar_state="expanded",
)
# 初始化
init_session_state()
# 页面标题和 Logo
st.title(PAGE_TITLE)
if os.path.exists(LOGO_PATH):
st.logo(LOGO_PATH)
# 显示当前会话标识
st.text(f"当前会话:{st.session_state.current_session}")
# 渲染侧边栏
render_sidebar()
# 渲染聊天区域
render_chat_history()
render_chat_input()
if __name__ == "__main__":
main()
总结
整章走下来,主线其实只有四条:
最后落到项目上,AI 智能伴侣覆盖了四大核心功能:大模型基础对话交互、会话记忆、伴侣人设定制、全量会话管理。技术栈横跨大模型部署方案、HTTP 协议、接口调试与 SDK 调用、Streamlit、Python 文件操作与时间处理——它是一个把 Python 基础语法真正串起来的综合性实战项目。
关于我
13 年 IT 全栈,.NET、VSTO、Python 都做,主业是帮企业和团队把重复劳动自动化——报表一键生成、数据对接、文档批量处理、工具定制。
本科物理化学、硕士计算机化学,在一线实验室待过 13 年,现在主要服务药企和实验室:SOP、合规、样本流、仪器数据接口这些词不用你解释。
这几年做得比较多的是这几类:自动化工具定制(VSTO / Office / Python)、数据对接与 LIMS 咨询、技术陪跑。
不是外包码农,是听得懂业务的自己人。有同类场景的朋友欢迎评论区聊聊,先聊清楚再动手不迟。
网硕互联帮助中心

评论前必须登录!
注册