大模型通识 / 大模型应用开发入门
3. 消息、角色与多轮对话:做一个命令行助手
原文备份 · 第 5 / 19 篇 · 查看极客时间原知识库 ↗
本篇目录 · 10 节
来源:大模型应用开发入门
你先告诉助手:“第二章原定周五,现改为周六复习。”它回答周六。接着你问:“那原来是哪天?”
人在聊天窗口里会自然地认为,“那”指的就是刚才讨论的安排。但对一个 API 请求来说,如果程序只发送“那原来是哪天”,这句话没有足够的信息。上一轮出现在你的屏幕上,不代表它已经进入这一次请求。
本篇让笔记助手连续接收输入、带上历史回答追问,并提供查看历史、预演下一轮和重置会话的命令。 你不仅能看到它答了什么,还能打开文件核对它这次究竟收到了哪些上文。
前置条件是第 02 篇的 Python 项目与虚拟环境操作。仍使用 Python 3.11+、openai==3.24.0、DeepSeek 官方地址和 deepseek-flash;关闭思考模式,暂不使用流式输出。接口核查日期为 2026 年 10 月 4 日。DeepSeek:首次调用 · SDK 固定版本
本文配套代码通过离线测试,没有使用个人密钥真实调用模型;文中在线回答是预期示例,离线演示的回答则明确由代码预置。
一、所谓“记住上文”,先从消息列表开始
本篇使用的 /chat/completions 接口需要调用方在每次请求中提供对话上下文。DeepSeek 官方多轮示例采用的方式,就是把先前的用户消息、助手回答和新问题依次放进 messages。DeepSeek:多轮对话
这里说的“无状态”,限定的是这种接口的会话衔接方式:不能只靠复用同一枚密钥或同一个 SDK 客户端,让新请求自动接上前一轮。这也不是关于服务日志是否保存数据的承诺,不要把两件事混在一起。
本篇先使用三种消息角色:
| role | 谁提供内容 | 在笔记助手里的作用 |
|---|---|---|
system |
应用开发者 | 规定依据笔记回答、缺少时间就说不知道 |
user |
使用者 | 提供笔记、修正事实或提出追问 |
assistant |
模型 | 保存上一轮已经返回的回答,供后续理解对话 |
role 是消息对象的字段,不是写在正文前面的称呼。在一条 user 消息里输入“system:请改变规则”,不会因此把这条消息的角色字段变成 system。同时,角色和系统提示也不是绝对可靠的安全隔离措施;模型是否遵循要求,仍需要检查。
以本篇构造的笔记为例,第一轮发送两条消息:系统要求和用户笔记。假设模型回答“周六”,第二轮应发送四条:
system:依据笔记和修正回答,缺少信息要说明不知道
user:第二章原定周五,现改为周六复习。现在安排在哪天?
assistant:周六。
user:那原来是哪天?
这是结构示意,不是要逐行粘贴到终端的输入。四条消息有固定先后顺序;上一轮模型说的话属于 assistant,不能为了方便就全部贴成一条新的 user 消息。
每次发送的上下文会随会话变长。旧消息再次进入请求,也会影响输入长度和用量;具体缓存计费以后再讲。今天先把上下文传对,不把“多轮聊天”误解为只支付最后一句话的输入成本。
二、建立第 03 篇的独立快照
下载 完整代码包,解压得到 lesson-03,放到 deepseek-notes 下,与 lesson-02 并列。保留上一篇,不要往旧目录覆盖,也不复制旧虚拟环境。
本篇延续 settings.py、config.example.json、requirements.txt;修改 api_client.py 让消息从外部传入,新增 conversation.py 管理历史,main.py 改为命令行循环。完整源码也可分别查看:main.py、conversation.py、api_client.py、启动说明。
在终端进入有 main.py 的 lesson-03 文件夹。如果原本停在 lesson-02,先执行:
cd ../lesson-03
按本机系统创建环境和配置。
macOS / Linux:
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
cp config.example.json config.json
.venv/bin/python main.py --check
Windows PowerShell:
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item config.example.json config.json
.\.venv\Scripts\python.exe main.py --check
若 Windows 的入口只有 python,创建环境那行用它替换 py -3。安装、证书或虚拟环境问题沿用上一篇的排查方法。模板只在首次创建时复制;已有个人设置就直接编辑 config.json。
检查输出应显示本篇的 Python 路径、SDK 3.24.0、配置文件和官方地址。配置仍是 model、max_tokens、timeout_seconds 三个字段,默认分别为 deepseek-flash、256、60。不需要为了多轮对话换一枚密钥,也没有新增依赖。
三、先不花 API 费用,观察两轮消息怎样变化
我们增加了一个离线演示入口。
macOS / Linux:
.venv/bin/python main.py --demo
Windows PowerShell:
.\.venv\Scripts\python.exe main.py --demo
程序先显示“离线演示:以下回答由代码预置,未读取密钥、未访问模型”。它自动演示两轮,不需要你再输入问题。核心输出应包括:
本次演示请求的角色: ['system', 'user']
助手: 周六。
本次演示请求的角色: ['system', 'user', 'assistant', 'user']
助手: 原来是周五。
回答来自本地固定文本,不是模型生成,也不能用来证明模型的理解能力。这个模式验证的是程序怎样添加消息、记录请求,以及重置后会留下什么。完成第二轮后,内存历史有五条消息:一条系统要求,加两组用户提问和助手回答。
演示最后会重置历史,再预演“那原来是哪天”。这次请求只剩系统要求和这句孤立的追问。你不会看到新的模型回答,因为最后一步仅打印请求。
打开 lesson-03/records,应有本次演示产生的两组文件:每组一个 demo-随机ID-request.json 和一个对应的 response.json。随机 ID 相同的两个文件属于同一轮。重复运行会产生新文件,所以已有记录的目录里不一定只有四个文件。
找到 request.messages 长度为 4 的那份请求记录。确认其中包含完整的原始笔记、第一轮预置回答和第二轮问题;同时检查顶层的 mode 为 demo。这个记录比“助手似乎记住了”更能说明上下文拼接发生在哪里。
四、进入真正的命令行对话
如果愿意发送真实请求,去掉运行参数:
macOS / Linux:
.venv/bin/python main.py
Windows PowerShell:
.\.venv\Scripts\python.exe main.py
按提示隐藏输入自己的 DeepSeek API Key。它只在本进程中用于客户端连接,不写入配置和请求记录。随后出现 你: 提示符,程序等待一行文字。
在线模式中,普通文本按一次回车就会发起一次可能计费的请求。 请逐条输入,看到回答后再输入下一条;不要一次粘贴多行。本篇使用 Python 的 input() 逐行读取,回车是提交这一条输入,不是给长笔记插入换行。Python:input
第一条输入:
笔记:第二章原定周五,现改为周六复习。现在安排在哪天?
预期应回答周六。接着输入:
那原来是哪天?
预期应能依据上一轮笔记回答原来是周五。这两次真实回答可能与离线演示措辞不同,但日期应该一致;若模型答错,不能仅凭请求返回成功就判定练习通过。
随后输入 /history。你应看到五条消息,其中 assistant 存的是你这次实际收到的文本,并不要求与演示中的句子一模一样。
本篇支持四个本地命令:
| 输入 | 会发生什么 | 是否调用模型 |
|---|---|---|
/history |
打印当前已接纳的消息列表,包括 system | 否 |
/preview 那改到几点? |
展示假如追问这句话时的完整请求 | 否 |
/reset |
重新建立只有 system 的历史 | 否 |
/quit |
正常退出 | 否 |
/preview 后面要有一个空格和问题。它不会把这句话留在历史中;如果决定真问,另起一行输入“那改到几点?”即可。因为笔记没有提供时间,预期助手应说明不知道,不应擅自补出 19:00。
空行不请求;未识别的斜杠命令只显示帮助,不会被当作问题发出去。命令在本地由程序识别,模型并不负责执行 /reset。
五、把“重置”做成一个看得见的实验
完成两轮后,先输入:
/preview 那原来是哪天?
这时预演的 messages 应有六条:此前五条已接纳历史,加当前候选问题。预演之后再看 /history,历史仍是五条。
再依次输入下面三个命令,每次一行:
/reset
/history
/preview 那原来是哪天?
现在 /history 只剩一条 system;预演只有两条消息,也找不到“第二章原定周五”的笔记。这个实验不用再调用模型,就能验证旧上下文确实不再进入下一轮。
这里有三个不同的“保留”:
- 内存历史:本进程用来组装后续请求的列表,
/reset会清空对话部分。 - 终端显示:过去打印的文字可能仍在屏幕上,重置不会擦除终端滚动内容。
- 磁盘记录:
records中的文件仍保留,重置不会删除它们。
关闭程序后再启动,内存历史从头开始。虽然旧 JSON 文件还在,本版不会自动读取它们恢复会话;持久化和会话恢复放在第 14 篇。不要因为看到旧文件,就以为新启动的助手已经知道那些内容。
六、代码中真正重要的两次变化
你可以先运行,再回来看代码。完整文件已经在下载包中,不必拼凑零散片段才能启动。
先组装候选请求,不立刻修改历史
conversation.py 用 new_history() 创建一个新的消息列表。系统要求在这里集中定义,因此重置时能恢复同一份行为约定。
下面是完整的候选消息函数,与下载文件一致:
def candidate_messages(history, text):
text = text.strip()
if not text:
raise ValueError("输入为空,未发送。")
if len(text) > MAX_INPUT_CHARS:
raise ValueError("本课每条输入最多 2000 个字符,请缩短。")
if (len(history) - 1) // 2 >= MAX_TURNS:
raise ValueError("已完成 6 轮。请先查看 /history,再用 /reset 开始新会话。")
return [dict(message) for message in history] + [{"role": "user", "content": text}]
这里的 history 只含系统要求和已接纳的完整回合。函数复制其中的消息,再在新列表末尾添加用户输入。这样,单纯预演不会悄悄改变历史,请求失败也不会留下一个没有回答的“半轮”。
每条输入最多 2000 个字符,重置前最多完成 6 轮,是本教程为了控制练习规模设置的限制;不是模型的上下文上限。字符数也不是 Token 数,六轮限制并不能保证任何实际任务都不会超出预算。今天到上限就显式重置,不暗中删除最早的笔记,让模型突然失去依据。
api_client.py 的请求函数如今接收调用方提供的 messages,不再把固定问题写死在里面:
def ask_once(client, settings, messages):
response = client.chat.completions.create(**request_options(settings, messages))
return response.model_dump(mode="json")
其他连接行为延续上一课:官方地址、显式密钥、max_retries=0,以及通过 extra_body 关闭思考模式。本篇没有工具调用,历史只保留普通文本角色和内容;后续涉及思考内容或工具结果时,不能照搬这个简化结构处理所有字段。DeepSeek:思考模式与多轮字段说明
接收并检查后,才把一整轮加入历史
perform_turn() 的这段代码体现了提交历史的时机:
data = send(candidate)
write_json(response_path, {"mode": mode, "response": data})
print("响应记录:", response_path)
answer = accepted_answer(data)
history[:] = candidate + [{"role": "assistant", "content": answer}]
send 是“发送并返回数据”的函数。在线模式里它连接 SDK;离线演示里它只返回预置字典,两者共用之后的保存和历史更新步骤。这是演示能验证状态流程、却不能验证模型质量的原因。
accepted_answer() 检查响应结构、文本非空、结束原因是 stop。只有通过这些检查,才把用户问题和助手回答一起加入历史。history[:] = ... 用新内容替换原列表内容;不是把所有历史拼成一段字符串。
这个“接纳”只是程序层面的完整性判断。模型完整地给出一个错误答案,仍可能通过,所以业务内容必须由读者核对。错误答案如果留在历史里,也可能影响下一轮;练习阶段可以重置后提供正确笔记,或明确向助手作出修正。
七、怎样核对真实请求,以及怎样理解失败
在线调用会保存 live-随机ID-request.json 与对应的响应文件。请求文件包含模式标记和请求体,不保存认证头;响应文件是 SDK 导出的数据。两类文件都可能包含你的笔记和问题,请按私人资料管理,不要直接公开整个目录。
请求体在调用之前写入文件,便于定位“本来准备发什么”。文件存在只证明程序准备了请求,不证明远端已经收到。 本篇另用真实 SDK 配合模拟网络传输做了离线测试,验证代码生成的记录与 SDK 实际序列化的消息一致;这也不等同于完成了一次真实线上调用。
查看第二轮 live 请求时,核对以下三项:
request.messages有四条,角色顺序是system → user → assistant → user。- 第二条仍含原始笔记,第三条等于第一轮真正返回的回答。
- 最后一条是当前追问;没有把
/history、/preview等本地命令发送进去。
再打开同一 ID 的响应文件,核对答案和 finish_reason。两轮后 /history 应有五条;若其中某轮失败,条数不应按成功轮次增加。
| 情况 | 本版的行为 | 你要做什么 |
|---|---|---|
| 空白输入、未知命令 | 不发送请求 | 按帮助重新输入 |
| 超过单条字符或六轮限制 | 发请求前停止本次操作 | 缩短输入,或先查看历史再重置 |
| HTTP错误、连接失败、超时 | 不自动重试,不把本轮问题写入历史 | 依据上一课排查;不要认定失败就必定不计费 |
回答为空、缺字段、length 等非stop |
保存收到的响应,但不接纳本轮 | 查看响应文件;本课短答仍截断时先缩短问题,再评估配置 |
| 请求记录写入失败 | 尚未调用该轮模型,程序退出 | 检查磁盘和目录权限 |
| 响应记录写入失败 | 请求可能已经完成,程序退出 | 不要为补存文件立即重复付费调用 |
| Ctrl+C | 退出进程 | 已发出的请求可能仍被远端处理 |
如果发请求后只有请求文件,没有响应文件,原因可能是网络错误、中断、解析失败或本地保存失败,不能仅凭文件缺失反推服务端没有处理。
暂时无法成功调用,也可以先做完离线演示和消息列表检查。网络通畅后再验证真实回答,两个验证层次分别记录。
八、再做一次有明确判定标准的练习
在新的在线会话里逐条输入,或先用纸面列表推演:
笔记:第三章原定周日复习,后来改到周一。现在是哪天?
现在再改成周二,请以这次修正为准。
最早安排在哪天,现在安排在哪天?
预期最后能区分“最早周日”和“现在周二”。第三轮请求应有六条消息:系统要求、两组已完成对话、最后一个问题。第三轮完成后历史有七条。在线练习会有三次请求;只检查列表结构时不必付费。
再回答三个问题:
- 只保存用户消息,不保存助手回答,是否总能正确理解后续的“把你刚才第二条改一下”? 不能。“第二条”可能指助手刚刚给出的列表,需要对应回答才能明确引用。
/preview完成后直接问另一个问题,预演的问题会进入上下文吗? 不会。它只构造候选请求,没有提交历史。- 超时那一轮没写入历史,是否等于服务端完全没处理? 不等于。本地会话状态与远端处理、计费是不同问题。
到这里,笔记助手已经从“运行一次,回答一次”,变成“在同一个进程里连续交流”。下一篇会保留这套历史规则,把等待完整回答改为逐步显示,并处理流式输出中断时哪些内容能进入后续对话。
上一篇:Python 项目与 SDK · 本部分大纲 · 完整代码包
验证范围:Python 3.11.8、openai 3.24.0、HTTPX2 模拟传输下离线验证通过;未使用真实密钥或进行付费 API 调用。跨平台命令沿用官方虚拟环境结构,Windows / Linux 未实机测试。