从 0 到 1:用一个最小 Python Agent,搞懂 AI 对话程序的基本原理

从 0 到 1:用一个最小 Python Agent,搞懂 AI 对话程序的基本原理

如果你是第一次接触 AI 编程,这篇文章会带你用一个非常小的项目,理解”AI 对话程序”到底是怎么工作的。

这个项目不复杂,但它覆盖了最核心的一条链路:
读取配置 → 调用模型 API → 拿到回复 → 在终端输出。

项目地址:https://git.liupx.com/study/openai_demo_first
更多信息(代码、更新、使用说明)可以访问这里。


1. 这篇文章你会学到什么

读完后,你应该能回答这几个问题:

  • 什么是”OpenAI 兼容 API”
  • 为什么要在 .env 里放 API KeyBASE_URL
  • AI 回复为什么能”一个字一个字蹦出来”(流式输出)
  • temperaturetop_p 这些参数到底在控制什么
  • 一个最小 AI Agent 的执行流程是什么

2. 先建立一个最小认知模型

先别看代码,先理解 4 个核心概念。

2.1 模型(Model)

你可以把模型理解成一个超大号文本补全器
你给它上下文,它预测下一段最合理的文本。

1
2
3
4
5
输入: "今天天气真"
模型预测: "好啊" / "糟糕" / "不错" ...

输入: "Python是一门"
模型预测: "编程语言" / "很流行的语言" / "简洁的语言" ...

2.2 消息(Messages)

聊天接口不是只传一句话,而是传一个消息列表。每条消息有角色和内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────┐
│ Messages 列表 │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ role: "system" │ │
│ │ content: "你是一个严谨的学习教练" │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ role: "user" │ │
│ │ content: "什么是Python?" │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ role: "assistant" │ │
│ │ content: "Python是一门编程语言..." │ │ ← 多轮对话时
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

三种角色说明:

  • system:给模型设定角色和规则(像导演给演员的剧本要求)
  • user:用户问题(像你问老师的问题)
  • assistant:模型历史回答(多轮对话时需要带上,让模型记住上下文)

2.3 API(程序访问模型的方式)

我们不是在本地跑大模型,而是通过 HTTP 接口调用远程模型服务。

1
2
3
4
5
6
7
8
graph LR
A[你的程序] -- HTTP请求 --> B[模型服务API]
B -- 返回结果 --> A
C[.env文件] -->|提供密钥和地址| A

style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#e8f5e9

这个项目使用的是 OpenAI Python SDK,但可以连”OpenAI 兼容接口”(如 DeepSeek、阿里通义千问等)。

2.4 Token(模型处理文本的基本单位)

模型按 token 处理文本,不是按”字数”。

Token 是什么?

  • 1 个 token ≈ 0.75 个英文单词,或约 1-2 个汉字
  • 模型把文本切分成小片段(token)来理解和生成
1
2
3
4
5
6
7
8
9
10
11
示例:这句话有多少个 token?

"我爱学习编程"

可能的切分方式:
┌────────────────────────────┐
│ 我 │ 爱 │ 学习 │ 编程 │
│ token1 │ token2 │ token3 │ token4 │
└────────────────────────────┘

约 4-6 个 token(取决于具体分词器)

max_tokens 控制的是”最多生成多少 token”,不是”最多多少个汉字”。


3. 这个项目做了什么

项目里有两个入口:

它们的核心事情完全一致:调用 client.chat.completions.create(...),然后把结果打印出来。


4. 运行前准备:为什么要 .env

配置文件示例(.env.example)是:

1
2
OPENAI_API_KEY=your_api_key
BASE_URL=https://your-openai-compatible-endpoint/v1

原因很简单:

配置项 作用 为什么需要
OPENAI_API_KEY 身份凭证 没有它服务端不会让你调用
BASE_URL 接口地址 告诉 SDK 请求发到哪个模型服务

为什么用 .env 文件?

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────┐
│ 不推荐:直接写死在代码里 │
│ api_key = "sk-xxxxx" ← 容易泄露! │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ 推荐:使用 .env 文件 │
│ 1. 安全:不会被提交到 Git │
│ 2. 灵活:不同环境用不同配置 │
│ 3. 规范:业界标准做法 │
└─────────────────────────────────────────┘

把密钥放在 .env,比直接写死在代码里安全得多,也便于切换环境。


5. 主流程拆解(最重要)

5.1 整体流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flowchart TD
A[程序启动] --> B[加载 .env 文件<br/>load_dotenv]
B --> C[读取配置<br/>OPENAI_API_KEY<br/>BASE_URL]
C --> D{配置检查}
D -->|缺少API Key| E[错误提示并退出]
D -->|缺少BASE_URL| E
D -->|配置正常| F[创建客户端<br/>OpenAI client]
F --> G[组装消息<br/>messages列表]
G --> H[发起请求<br/>chat.completions.create]
H --> I{是否流式?}
I -->|是 stream=True| J[循环读取 chunk<br/>实时打印]
I -->|否 stream=False| K[等待完整结果<br/>一次性打印]
J --> L[输出完成]
K --> L

style E fill:#ffebee
style L fill:#e8f5e9
style J fill:#e3f2fd

5.2 核心代码讲解(最小可运行版本)

这是一个完整的最小示例,约 30 行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 导入需要的库
import os
from dotenv import load_dotenv
from openai import OpenAI

# 1. 加载环境变量
load_dotenv()

# 2. 读取配置
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("BASE_URL")

# 3. 创建客户端
client = OpenAI(api_key=api_key, base_url=base_url)

# 4. 组装消息
messages = [
{"role": "user", "content": "帮我设计一个早起计划"}
]

# 5. 发起请求(流式)
stream = client.chat.completions.create(
model="deepseek-chat", # 指定模型
messages=messages, # 消息列表
stream=True, # 开启流式输出
)

# 6. 实时打印结果
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)

你会发现,AI 应用的第一版往往就这几步。


6. 为什么会”流式输出”

6.1 原理解析

main.py 中,请求里用了:

1
stream=True

这意味着服务端会把回答切成多个小片段返回。

非流式 vs 流式对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌────────────────────────────────────────────────────────────┐
│ 非流式输出 (stream=False) │
├────────────────────────────────────────────────────────────┤
│ 等待... 等待... 等待... │
│ [10秒后] │
│ "完整的一段话一次性显示出来" │
│ │
│ 用户体验:等待时间较长,不知道是否在处理 │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 流式输出 (stream=True) │
├────────────────────────────────────────────────────────────┤
│ "完" -> "整" -> "的" -> "一" -> "段" -> "话" │
│ 一个字一个字实时显示出来 │
│ │
│ 用户体验:即时反馈,更流畅,像真人对话 │
└────────────────────────────────────────────────────────────┘

6.2 代码实现

流式处理的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 开启流式返回
stream = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "你好"}],
stream=True, # ← 关键参数
)

# 循环读取每个片段(chunk)
for chunk in stream:
# 检查是否有内容
if not chunk.choices:
continue

# 获取增量文本
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True) # 实时打印

所以你看到的效果就是:

  • 不是等整段回答全部生成完才显示
  • 而是边生成边显示,体验更像实时对话

7. 参数到底在调什么(面向新手版)

main_cli.py 支持几个常见参数:

7.1 参数速查表

参数 作用 取值范围 建议值
--model 使用哪个模型 字符串 deepseek-chat
--system 给模型设定角色 任意文本 根据场景设置
--temperature 随机性/发散程度 0.0 ~ 2.0 0.2~0.8
--top-p 核采样控制 0.0 ~ 1.0 0.9 左右
--max-tokens 最多生成多少token 正整数 视需求而定
--no-stream 关闭流式输出 标志位 需要完整结果时使用

7.2 Temperature 详解

Temperature 是什么?

Temperature 控制模型输出的随机性和多样性。想象一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Temperature = 0  (完全确定性)
┌────────────────────────────────┐
│ 像"背诵课文" │
│ 每次回答几乎完全相同 │
│ 适合:需要精确、稳定的答案 │
└────────────────────────────────┘

Temperature = 0.7 (适中随机)
┌────────────────────────────────┐
│ 像"正常交谈" │
│ 有一定变化,但保持逻辑一致 │
│ 适合:日常对话、创意任务 │
└────────────────────────────────┘

Temperature = 1.5 (高随机性)
┌────────────────────────────────┐
│ 像"头脑风暴" │
│ 非常有创意,但可能不太连贯 │
│ 适合:需要大量创意的场景 │
└────────────────────────────────┘

7.3 实际效果对比

使用不同的 temperature 调用同一个问题:

1
2
3
4
5
6
7
# 低温度:0.2(更稳定、更保守)
python src/agent/main_cli.py "用一句话介绍Python" --temperature 0.2
# 输出:Python是一门简洁高效、易学易用的高级编程语言。

# 高温度:0.9(更多样、更发散)
python src/agent/main_cli.py "用一句话介绍Python" --temperature 0.9
# 输出:Python是一门简洁高效、易学易用的通用编程语言。

7.4 新手建议

  1. 先只改 prompt--system
  2. temperature 先从 0.20.8 小范围试
  3. 不要一开始同时乱调多个参数,不然很难判断变化原因
  4. 需要稳定答案时用低 temperature,需要创意时用高 temperature

8. 常见报错怎么理解

8.1 “未检测到 OPENAI_API_KEY”

现象:

1
未检测到 OPENAI_API_KEY。请在 .env 文件中配置后重试。

原因和解决:

  • .env 文件不存在或没有配置 OPENAI_API_KEY
  • .env 文件不在项目根目录
  • 检查步骤:确认 .env 文件存在,内容格式正确

8.2 “未检测到 BASE_URL”

现象:

1
未检测到 BASE_URL .env 文件中配置后重试。

原因和解决:

  • 接口地址缺失,SDK 不知道请求发去哪
  • 确保在 .env 中配置了 BASE_URL

8.3 连接失败 / 429 / 配额不足

现象:

1
调用失败:HTTPSConnectionPool(...) / 429 Client Error

可能原因:

问题类型 检查项
服务地址不可达 BASE_URL 是否正确,网络是否通畅
网络或代理问题 是否需要配置代理,防火墙是否拦截
额度不足或限流 账户是否有足够配额,是否触限流

排查顺序:

  1. 先检查配置(.env 文件)
  2. 再检查网络(是否能访问 API 地址)
  3. 最后看平台账单和配额

9. 给零基础同学的 3 个小实验

直接用 main_cli.py 做:

实验 1:同一个问题,换 system prompt

1
2
3
4
5
# 温和的老师版本
python src/agent/main_cli.py "给我一份一周学习计划" --system "你是温和的老师,说话要鼓励学生"

# 严格的教练版本
python src/agent/main_cli.py "给我一份一周学习计划" --system "你是严格的教练,要求学生必须执行"

观察点: 同样的问题,不同的人设,回答风格有什么不同?

实验 2:对比流式与非流式

1
2
3
4
5
# 流式输出(默认)
python src/agent/main_cli.py "解释什么是提示词工程"

# 非流式输出
python src/agent/main_cli.py "解释什么是提示词工程" --no-stream

观察点: 两种方式在用户体验上有什么区别?

实验 3:调 temperature 看风格变化

1
2
3
4
5
# 低温度
python src/agent/main_cli.py "给我3个自我介绍版本" --temperature 0.2

# 高温度
python src/agent/main_cli.py "给我3个自我介绍版本" --temperature 0.9

观察点: 回答的多样性有什么变化?


10. 运行效果示例

以下是实际运行效果:

1
2
3
4
5
6
7
8
$ python src/agent/main.py
Assistant: 为你设计一个科学的早起计划,帮助你在不牺牲健康的前提下,逐步养成早起习惯...

### **第一步:明确早起的目的(关键!)**
早起本身不是目的,获得高质量的早晨时间才是...

### **第二步:制定渐进式早起方案(核心步骤)**
**切勿突然提前1-2小时起床!** 这会导致睡眠不足...

11. 一句话总结

这个项目本质上是在教你一件事:
AI 应用的第一步,不是复杂算法,而是先打通调用链路并理解输入/输出机制。

当你真正理解这条链路后,再去学多轮记忆、RAG、Agent 工具调用,会轻松很多。


12. 建议的下一步学习顺序

1
2
3
4
5
6
7
8
9
10
11
graph LR
A[当前阶段<br/>单次调用] --> B[多轮对话<br/>保存历史messages]
B --> C[提示词工程<br/>设计稳定的Prompt]
C --> D[RAG<br/>引入外部知识库]
D --> E[复杂Agent<br/>工具调用与编排]

style A fill:#e8f5e9
style B fill:#e3f2fd
style C fill:#fff3e0
style D fill:#fce4ec
style E fill:#f3e5f5
  1. 让程序支持多轮对话(保存历史 messages
  2. 设计更稳定的提示词模板(Prompt Template)
  3. 引入外部知识库(RAG)
  4. 再考虑更复杂的 Agent 编排

先小步快跑,持续可运行,比一次做”大而全”更重要。


更多信息与项目最新内容请访问:https://git.liupx.com/study/openai_demo_first