一个项目带你入门AI应用开发01
第 1 课:从零开始,让 AI 和你对话
1.1 你的目标
写一个 Python 程序,在终端里输入文字,AI 会回答你。
You: 你好
AI: 你好!有什么可以帮你的?
You: 查一下订单
AI: 抱歉,我目前无法查询订单信息...
1.2 最直接的方式:一行代码调 API
所有大模型(DeepSeek、GPT、通义千问)都提供 HTTP API。最简单的调用方式:
import requests
resp = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers={
"Authorization": "Bearer sk-你的key",
"Content-Type": "application/json",
},
json={
"model": "deepseek-chat",
"messages": [
{"role": "user", "content": "你好"}
],
},
)
print(resp.json()["choices"][0]["message"]["content"])
把这段代码保存为 test.py,把 sk-你的key 替换成你从 DeepSeek 官网申请的 API Key,然后运行:
python test.py
如果看到 AI 回复了,说明一切正常。
为什么要从这里开始?
不是要你记住这个 JSON 结构,而是要理解一个事实:所有 AI Agent 的底层就是一次 HTTP 请求。不管多复杂的系统,最终都是这个东西。
1.3 第一个问题:API Key 暴露了
上面那段代码里,API Key 是直接写在字符串里的。如果你把这个文件传到 GitHub,任何人都能看到并使用你的 Key。
为什么不能把 Key 写死在代码里?
- 你可能会把代码传到 GitHub(不管有意还是无意)
- 不同环境(开发/测试/生产)要用不同的 Key,写死意味着改代码
- 换 Key 要重新部署
正确的做法:环境变量 + .env 文件
import os
from dotenv import load_dotenv
# 读取 .env 文件(如果有的话)
load_dotenv()
API_KEY = os.getenv("LLM_API_KEY")
BASE_URL = os.getenv("LLM_BASE_URL", "https://api.deepseek.com/v1")
MODEL = os.getenv("LLM_MODEL", "deepseek-chat")
把 Key 写在 .env 文件里:
LLM_API_KEY=sk-你的key
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat
然后把 .env 加到 .gitignore:
.env
这样就安全了。load_dotenv() 会自动读取 .env 文件的内容。
更进一步:用 pydantic-settings 管理
手动 os.getenv 每个变量,写起来很啰嗦。pydantic-settings 可以自动从环境变量和 .env 文件读取配置:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
llm_api_key: str = ""
llm_base_url: str = "https://api.deepseek.com/v1"
llm_model: str = "deepseek-chat"
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
settings = Settings()
然后代码里用 settings.llm_api_key 就行。所有配置集中管理,一目了然。
1.4 第二个问题:重复代码
每次调 API 都要写:
- 拼 URL
- 拼 headers
- 拼 body
- 发请求
- 解析 response
如果代码里每个调 API 的地方都这么写一遍,有 10 处就要写 10 遍。想改点什么(比如加个 timeout)就要改 10 个地方。
为什么重复代码是问题?
想象一下,某天你要给所有 API 调用加一个 timeout=30 的参数。如果代码写在一个函数里,改 1 处就够了。如果散落在 10 个地方,你很可能漏掉某个地方——然后某个功能在特定场景下静默失败。
正确的做法:封装成函数
def call_llm(messages, temperature=0.7):
"""一次封装,到处调用"""
resp = requests.post(
f"{BASE_URL}/chat/completions",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"model": MODEL,
"messages": messages,
"temperature": temperature,
},
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
现在每次调用只需要一行:
reply = call_llm([{"role": "user", "content": "你好"}])
print(reply)
封装的边界在哪里?
call_llm 函数的接口设计值得你思考:
- 参数:只暴露调用方需要关心的东西(
messages和temperature),其他内部细节隐藏了 - 返回值:只返回 AI 回复的内容,JSON 解析的细节在里面处理了
- 异常:
resp.raise_for_status()让调用方可以通过 try/except 统一处理错误
这不是"为了封装而封装",而是让调用方不需要知道 API 的细节。第 4 课我们会在不改变调用方式的情况下,给 call_llm 加上 tools 参数支持——调用方代码几乎不需要改。
1.5 第三个问题:只能问一次
现在的程序跑完就退出了。一次对话就要重新启动一次程序。
正确的做法:对话循环
def main():
print("AI 客服终端(输入 /quit 退出)")
messages = [
{"role": "system", "content": "你是一个友好的电商客服助手。"}
]
while True:
user_input = input("\nYou: ").strip()
if not user_input:
continue
if user_input == "/quit":
break
messages.append({"role": "user", "content": user_input})
reply = call_llm(messages)
print(f"AI: {reply}")
messages.append({"role": "assistant", "content": reply})
注意 messages 数组的变化:
开始时: [system]
第1轮: [system, user_1] → AI 回复 → [system, user_1, assistant_1]
第2轮: [system, user_1, assistant_1, user_2] → AI 回复 → ...
每次对话都把历史传回去,这样 AI 才能"记住"前面说了什么。这是"多轮对话"的本质。
1.6 完整代码
# 01-basic-chat/chat.py
import requests
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
llm_api_key: str = ""
llm_base_url: str = "https://api.deepseek.com/v1"
llm_model: str = "deepseek-chat"
model_config = {"env_file": "../.env", "env_file_encoding": "utf-8"}
settings = Settings()
def call_llm(messages, temperature=0.7):
"""通用 LLM 调用函数"""
url = f"{settings.llm_base_url.rstrip('/')}/chat/completions"
headers = {
"Authorization": f"Bearer {settings.llm_api_key}",
"Content-Type": "application/json",
}
body = {
"model": settings.llm_model,
"messages": messages,
"temperature": temperature,
}
resp = requests.post(url, headers=headers, json=body, timeout=60)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def main():
messages = [{"role": "system", "content": "你是一个友好的电商客服助手。"}]
print("AI 客服终端(输入 /quit 退出)")
while True:
user_input = input("\nYou: ").strip()
if not user_input:
continue
if user_input == "/quit":
break
messages.append({"role": "user", "content": user_input})
try:
reply = call_llm(messages)
print(f"AI: {reply}")
messages.append({"role": "assistant", "content": reply})
except Exception as e:
print(f"出错了:{e}")
if __name__ == "__main__":
main()
本课知识点
| 概念 | 你做了什么 | 为什么 |
|---|---|---|
| API 调用 | HTTP POST → /v1/chat/completions |
所有大模型都兼容这个接口,学一次通吃所有 |
| 配置管理 | pydantic-settings + .env | Key 不写进代码,不同环境用不同配置 |
| 函数封装 | 抽出 call_llm |
一处修改,全局生效;调用方不关心内部细节 |
| 消息结构 | messages: [system, user, assistant] | LLM 本身无状态,上下文全靠 messages 数组传递 |
| 参数设计 | 暴露调用方关心的,隐藏内部细节 | 接口稳定,后续扩展(如 tools)不影响已有调用方 |
课后作业
- 把
temperature改成 0.1 和 1.0 分别试试,观察回复差异 - 加一个
/new命令,清除对话历史重新开始 - 去掉
messages中的 system 消息,看看 AI 的行为有什么变化
面试可能会问
"LLM API 调用的 messages 里 system、user、assistant 三种 role 的区别是什么?"

浙公网安备 33010602011771号