第一阶段:直连大脑 — 模型层开发

8 阅读22分钟

教学寄语

在这一阶段,我们将剥离所有繁杂的框架,直接与大模型的 API 对话。这就像学习武功前的站桩——虽然枯燥,但这是理解 AI 应用“内力”运行方式的唯一途径。理解了这一层,后面学习 LangChain 等框架时,你看到的才是“逻辑”,而不是“黑盒”。

本讲义的核心不是“背 API”,而是建立一个底层心智模型:大模型应用本质上是在代码中组织上下文、发送请求、接收输出、再把输出转成可被程序可靠处理的数据结构。

因此,本阶段可以理解为 Agent 开发的“模型层地基”:只有知道模型如何接收 messages、如何被 prompt 约束、如何输出 JSON/对象,后续学习工具调用、LangChain、工作流编排时才不会被框架包住视野。

你将获得的能力

• 能用官方 SDK 或兼容 OpenAI 协议的服务,完成一次最小可运行的模型调用。

• 能解释 system、user、assistant 三类角色在对话历史中的作用。

• 能用 Few-Shot、温度参数、格式约束来降低输出不稳定性。

• 能用 Pydantic 把“自然语言输出”升级为“代码对象”。

• 能理解 CoT/分步推理在复杂任务中的应用边界。

学习路线图

  1. Task 1.1:先让模型开口说话,理解一次请求的完整路径。

  2. Task 1.2:让模型按格式说话,把 Prompt 当成可维护的“软规则”。

  3. Task 1.3:让模型输出可验证对象,用 Pydantic 建立“硬契约”。

  4. Task 1.4:让模型学会分步解题,并把推理过程设计成可展示的结构。


  1. 前置准备:环境、密钥与工程习惯

本节学习定位

本节解决“代码跑不起来”的基础问题。不要急着写 Prompt,先确认 Python 环境、依赖、API Key 和模型服务地址都可用。

在开始写代码之前,请确认环境齐全:

• 语言环境:Python 3.10+。建议固定项目解释器版本,避免不同项目之间依赖冲突。

• 虚拟环境:推荐 Conda 或 venv。一个项目一个虚拟环境,是长期维护 AI 工程的基本习惯。

• 核心依赖:openai 用于请求模型,python-dotenv 用于加载 .env,pydantic 用于结构化数据验证。

pip install openai python-dotenv pydantic

为什么一定要使用 .env

API Key 属于敏感凭据。把它写进代码文件,最大的风险不是“自己看见”,而是后续提交 Git、发给同学、部署到服务器时被泄露。使用 .env 的意义是把“配置”和“代码”分离。

最佳实践

把 .env 加入 .gitignore;线上环境优先使用平台提供的环境变量;多人协作时只提交 .env.example,不提交真实密钥。

原始 .env 示例

LLM_API_KEY="sk-xxxxxxxxxxxxxxxx"
LLM_BASE_URL="https://api.openai.com/v1" # 若使用国内模型,请修改为对应的 Base URL
LLM_MODEL_ID="gpt-4o-mini" # 或 deepseek-chat 等

配置项解释

• LLM_API_KEY:模型服务的访问凭据。没有它,请求会被鉴权系统拒绝。

• LLM_BASE_URL:API 服务入口。使用兼容 OpenAI 协议的模型厂商时,通常要替换为对应的 Base URL。

• LLM_MODEL_ID:具体模型名称。不同厂商的模型 ID 命名方式不同,运行前要与控制台保持一致。

 # 错误排查示例

AuthenticationError: Incorrect API key provided
# 原因:LLM_API_KEY 没有设置、写错,或 .env 没有被 load_dotenv() 读取。
  1. 阶段目标:不靠框架,直接理解模型层

本节学习定位

本阶段有意不使用 LangChain、AutoGen 等上层框架,目的是让学习者先看清最底层的数据流。

目标可以概括为一句话:不依赖第三方 Agent 框架,仅使用官方 SDK 或兼容 SDK,完成与大模型的交互,并掌握结构化输出,实现“代码即提示,输出即对象”。

• 代码即提示:Prompt 不只是文案,而是被版本管理、复用、测试的工程资产。

• 输出即对象:模型输出不应停留在字符串层面,而要尽可能转为 dict、Pydantic 对象或业务对象。

• 模型无状态:你每次请求发什么上下文,它就知道什么上下文;没发的内容,它不会替你记住。

Task 1:环境搭建与初次对话

本节学习定位

学习目标:跑通第一个 AI 程序,理解一次 Chat Completion 调用背后的“角色扮演”和请求-响应流程。

应用场景

• 做一个最小版聊天机器人:接收用户输入,返回模型回复。

• 做一个代码问答助手:让模型扮演 Python 导师,回答初学者问题。

• 为后续 Agent 做底层封装:把模型调用包装成一个可复用函数。

请求流程心智模型

一次模型调用可以拆成四步:读取环境变量 -> 创建客户端 -> 构造 messages -> 发起请求并读取 response。只要其中任一环节出错,最终都可能表现为“模型没有回答”。

• client:负责知道“请求发给谁”和“用什么身份请求”。

• messages:负责告诉模型“当前对话到哪里了”。

• model:负责选择“让哪个模型来回答”。

• temperature:负责控制“回答的随机性”。

Python
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI(
    api_key=os.environ.get('LLM_API_KEY'),
    base_url=os.environ.get('LLM_BASE_URL')
)

response = client.chat.completions.create(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=[
        {"role": "system", "content": "你是一个只会说文言文的古代诗人"},
        {"role": "user", "content": "我是新手,请问 Python 里怎么打印 Hello World?"},
    ],
    temperature=0.7,
    stream=False
)

print(response.choices[0].message.content)

逐段拆解

  1. load_dotenv():把 .env 里的配置加载进当前进程环境变量。没有这一步,os.environ.get(...) 很可能返回 None。

  2. OpenAI(...):创建 SDK 客户端。api_key 用于鉴权,base_url 用于兼容不同模型服务商。

  3. messages:一个列表,列表中的每个元素都是一条消息。模型不会“自动记忆”,它只读取你这次发过去的列表。

  4. system:用于设定模型身份、规则和回答风格。这里把模型设定为“只会说文言文的古代诗人”。

  5. user:用户真正提出的问题。代码中让模型解释 Python 如何打印 Hello World。

  6. response.choices[0].message.content:读取第一个候选回答的正文。

运行结果演示

# Python 中欲显 “Hello World”,可书:

print("Hello World")

# 运行之,则屏上见 Hello World 矣。

注意:这只是可能输出。temperature=0.7 会保留一定创造性,因此模型的措辞可能每次略有不同。

关键概念补充

• Chat Completion 的 “Chat” 表示输入是对话历史,而不是单句补全。

• Completion 的本质仍是“根据上下文预测下一个最可能的输出”。

• choices 是列表,是因为 API 可以一次生成多个候选答案;常规应用通常先读取 choices[0]。

• stream=False 表示一次性拿完整回答;若改为 stream=True,则更适合做打字机效果。

避坑指南

messages 的顺序非常重要。通常是 system 在前,之后 user 与 assistant 按真实对话顺序交替追加。顺序混乱会影响回答质量,某些接口还会直接报错。

拓展案例:把一次性问答改成多轮对话

messages = [
    {"role": "system", "content": "你是一个耐心的 Python 老师。"},
    {"role": "user", "content": "Python 里的变量是什么?"}
]

first = client.chat.completions.create(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=messages,
    temperature=0.3
)

messages.append(first.choices[0].message)
messages.append({"role": "user", "content": "请用一个生活类比再解释一次。"})

second = client.chat.completions.create(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=messages,
    temperature=0.3
)

print(second.choices[0].message.content)

这个变体的重点是 messages.append(...)。多轮对话不是模型真的“记住了你”,而是你把上一轮 assistant 的回答重新发回去了。

Task 2:Prompt Engineering 代码化

本节学习定位

学习目标:解决模型“太随意”的问题,通过 Prompt、示例和温度参数控制输出格式,让自然语言接口更像一个可编程函数。

为什么需要 Prompt Engineering

自然语言很灵活,但程序需要稳定。人类能理解“还行”“不错”“挺垃圾”这些模糊表达,代码却更希望拿到固定字段,例如 sentiment、product、reason。Prompt Engineering 的任务,就是把模型从“自由聊天”拉回“按规则填表”。

• System Prompt:定义任务、角色、格式和禁忌。

• Few-Shot:给模型看几个输入-输出样例,让它模仿格式。

• temperature=0:降低随机性,适合分类、抽取、JSON 输出。

• json.loads:把字符串转成 Python 字典,但前提是输出必须是合法 JSON。

"""
情感分析器
我们要开发一个电商评论分析工具。
输入:用户评论。
输出:严格的 JSON 格式数据 { "sentiment": "好评", "product": "手机", "reason": "..." }。
Few-Shot + 结构化
"""
import os
import json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI(
    api_key=os.environ.get('LLM_API_KEY'),
    base_url=os.environ.get('LLM_BASE_URL')
)

# 1、定义System Prompt
system_instruction = """
你是一个电商数据分析师。你的任务是分析用户评论,提取关键信息。
输出要求:
1. 必须严格按照 JSON 格式输出。
2. 不要输出任何 JSON 之外的多余文字(如“好的,分析如下...”)。
3. JSON 字段必须包含:sentiment (好评/差评), product (商品名), reason (理由)。
"""

# 2、定义Few-Shot示例(教导模型如何回答)
example_user_1 = "评论:这双鞋穿起来很舒服,尺码标准,物流也快。"
example_assistant_1 = '{"sentiment": "好评", "product": "鞋", "reason": "穿着舒服,尺码标准,物流快"}'

example_user_2 = "评论:什么垃圾耳机,噪音很大,听不清。"
example_assistant_2 = '{"sentiment": "差评", "product": "耳机", "reason": "噪音大,音质差"}'

# 3、定义用户真实输入
real_user_input = "这手机屏幕太脆了,摔一下就碎,避雷!"

# 4、构建对话列表
messages = [
    {"role": "system", "content": system_instruction},
    # 第一轮对话
    {"role": "user", "content": example_user_1},
    {"role": "assistant", "content": example_assistant_1},
    # 第二轮对话
    {"role": "user", "content": example_user_2},
    {"role": "assistant", "content": example_assistant_2},
    # 真实用户提问
    {"role": "user", "content": real_user_input}
]

# 5、发起请求
response = client.chat.completions.create(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=messages,
    temperature=0,  # 强制模型选择概率最高的词,减少随机性
    stream=False
)

# 6、解析结果
output_text = response.choices[0].message.content
print("模型原始输出:", output_text)

try:
    parsed_data = json.loads(output_text)
    print("\n解析后的字典:", parsed_data)
    print("提取的情感:", parsed_data['sentiment'])
except json.JSONDecodeError:
    print("错误:模型输出不是合法的 JSON")

这段代码解决什么问题

它模拟了电商评论分析场景:输入一条用户评论,输出固定 JSON 字段。这样的代码常用于评论看板、客服工单分流、商品质量预警和用户反馈聚类。

运行结果演示

模型原始输出: {"sentiment": "差评", "product": "手机", "reason": "屏幕脆弱,摔一下就碎,用户建议避雷"}

解析后的字典: {'sentiment': '差评', 'product': '手机', 'reason': '屏幕脆弱,摔一下就碎,用户建议避雷'}
提取的情感: 差评

教学拆解:Prompt 的“软约束”

  1. system_instruction 是任务说明书。它告诉模型输出必须是 JSON,且不能夹带“好的,分析如下”之类的自然语言。

  2. example_user_1 / example_assistant_1 是第一个样例,帮助模型理解“舒服、尺码标准、物流快”应归为好评。

  3. example_user_2 / example_assistant_2 是第二个样例,帮助模型理解负面词和差评字段如何对应。

  4. real_user_input 是真实待分析文本。模型会根据前面两个样例补出同样结构的答案。

  5. try...except 捕获 json.loads 失败,避免模型偶尔输出非 JSON 时导致程序直接崩溃。

核心提醒

Prompt 约束属于“软约束”。它能显著提高格式稳定性,但不能 100% 保证模型永远输出合法 JSON。只要下游代码依赖结构化字段,就要准备异常处理或使用更强的结构化输出机制。

原始思维链示例(保持不变)

步骤1:用户提到了"屏幕脆""避雷"。
步骤2:这些词是负面的。
结果:{"sentiment": "差评"}

这个例子展示了 Few-Shot 里可以加入“分步判断”的思想:先识别关键词,再判断情绪,再输出结果。对于复杂分类任务,这比只给最终答案更容易稳定。

拓展案例:把评论路由到不同处理部门

system_instruction = """
你是一个售后工单分类助手。请把用户反馈分类为 JSON:
{"department": "物流/质量/客服/其他", "priority": "高/中/低", "summary": "一句话摘要"}
只输出 JSON,不要输出解释。
"""

messages = [
    {"role": "system", "content": system_instruction},
    {"role": "user", "content": "快递三天没动了,客服也没人回,我很着急。"}
]

response = client.chat.completions.create(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=messages,
    temperature=0
)

print(response.choices[0].message.content)

拓展案例输出示例

{"department": "物流", "priority": "高", "summary": "用户反馈快递长时间未更新且客服未响应"}

Task 3:结构化输出 —— 强制契约

本节学习定位

学习目标:解决模型输出不稳定导致 json.loads() 报错的问题,让模型输出尽可能直接变成 Python 对象。

Task 2 依靠 Prompt 约束模型输出 JSON;Task 3 更进一步:先在代码里定义数据结构,再要求模型按这个结构返回数据。这里的关键词是“契约”:字段名、字段类型、取值范围都提前写清楚。

• BaseModel:定义一个可验证的数据模型。

• Field(description=...):既是字段说明,也是给模型看的提取提示。

• List[str]:要求字段是字符串列表,而不是一段混杂文本。

• Literal[...]:限制字段只能从指定枚举值中选择。

版本兼容提醒

不同 SDK 版本和兼容 OpenAI 协议的服务商,对 Pydantic 作为 response_format 的支持并不完全一致。这里保留原始代码不变;如果本地运行出现 response_format 相关错误,可参考官方/服务商文档,把“新增兼容写法”作为替代实验。

"""
自动生成用户画像
假设我们要让模型分析一段文本,提取出用户的姓名、年龄、爱好列表和一个情感标签。
"""
from pydantic import BaseModel, Field
from typing import List, Literal
import os
import json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

# 第一步:先定义数据结构
# 1、定义数据模型——这就是契约
class UserProfile(BaseModel):
    name: str = Field(description="用户的姓名")
    age: int = Field(description="用户的年龄,必须是整数")
    # Field(description=...) 非常重要!这个 description 会直接发给模型看,指导它如何提取信息。
    interests: List[str] = Field(description="用户的兴趣爱好列表")
    # Literal[...] 限制了字段的取值范围,模型只能填这几个词,不能瞎编。
    sentiment: Literal["正面", "中性", "负面"] = Field(description="文本的情感倾向")

# 第二步:调用API解析
client = OpenAI(
    api_key=os.environ.get('LLM_API_KEY'),
    base_url=os.environ.get('LLM_BASE_URL')
)

# 1、用户输入
user_txt = "我叫张三,今年27岁,平时没事喜欢看小说和电视剧,感觉今天过的很充实!"

# 2、构造prompt
messages = [
    {
        "role": "system",
        "content": "你是一个用户画像提取专家。请根据用户输入,提取关键信息。"
    },
    {
        "role": "user",
        "content": user_txt
    }
]

# 3、发起请求
completion = client.chat.completions.create(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=messages,
    # 核心魔法:指定 response_format
    # 这告诉 API:“别给我字符串了,我要一个能解析成 UserProfile 类的 JSON”
    response_format=UserProfile
)

# 4、解析结果
# 解析结果 —— 不需要 try-catch json.loads 了!
# completion.choices[0].message.parsed 已经自动转换成了 Python 对象
profile = completion.choices[0].message.parsed

print("提取成功!")
print(f"姓名: {profile.name}")     # 直接点属性,有代码提示!
print(f"年龄: {profile.age}")
print(f"爱好: {profile.interests}")
print(f"情感: {profile.sentiment}")

运行结果演示

提取成功!
姓名: 张三
年龄: 27
爱好: ['看小说', '电视剧']
情感: 正面

字段设计为什么重要

  1. name: str 让姓名字段永远以字符串形式出现,方便写入数据库或展示。

  2. age: int 把年龄限定为整数。如果模型输出“二十七岁”,校验层就能及时发现问题。

  3. interests: List[str] 把多个兴趣拆成列表,后续可以做标签统计。

  4. sentiment: Literal["正面", "中性", "负面"] 防止模型输出“开心”“积极”“不错”等不可控同义词。

新增兼容写法示例:使用 parse 获取 Pydantic 对象

Python
completion = client.chat.completions.parse(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=messages,
    response_format=UserProfile
)

profile = completion.choices[0].message.parsed
print(profile.name, profile.age, profile.interests, profile.sentiment)

这段新增示例表达的是同一个目标:让返回结果直接拥有 profile.name、profile.age 这样的属性访问方式。实际项目中,请以当前 SDK 和模型服务商支持的写法为准。

应用场景迁移

• 用户画像:从聊天记录里提取姓名、年龄、职业、兴趣、购买意向。

• 简历解析:从文本中提取教育经历、技能、工作年限。

• 合同审查:抽取甲方、乙方、金额、日期、风险条款。

• 客服工单:抽取问题类型、紧急程度、是否需要人工介入。

拓展案例:订单信息抽取

from pydantic import BaseModel, Field
from typing import Literal

class OrderIssue(BaseModel):
    order_id: str = Field(description="订单编号")
    issue_type: Literal["退款", "换货", "物流", "质量", "其他"] = Field(description="问题类型")
    urgency: Literal["高", "中", "低"] = Field(description="紧急程度")
    summary: str = Field(description="一句话概括用户问题")

user_text = "订单 A20260424 到现在还没发货,客户明天就要用,麻烦尽快处理。"

这个案例把“用户画像”迁移成“订单问题抽取”。你会发现,结构化输出真正的价值不是某个字段,而是同一套范式可以快速迁移到不同业务。

Task 4:思维链 —— 让模型学会“慢思考”

本节学习定位

学习目标:面对多步骤问题时,让模型先分解任务,再给出结论,从而降低一步到位回答的错误率。

所谓思维链,在教学场景中可以理解为“把解题过程写在草稿纸上”。模型直接给答案时,容易把多个计算关系混在一起;让它分步输出,等于把复杂任务拆成一串小任务。

工程化表达

在面向用户的产品里,不一定要展示模型的所有内部推理。更稳妥的做法是让模型输出“可检查的解题步骤”或“摘要级推导过程”,既方便教学,也便于调试。

"""解题专家"""
from pydantic import BaseModel, Field
from typing import List, Literal
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI(
    api_key=os.environ.get('LLM_API_KEY'),
    base_url=os.environ.get('LLM_BASE_URL')
)

# 1、定义"带思考过程"的数据结构
class MathSolution(BaseModel):
    reasoning_steps: List[str] = Field(description="解题的详细步骤列表,每一步是一个字符串,展示你的思考过程。")
    final_answer: str = Field(description="经过推理后的最终答案,只需要输出结果")

# 2、准备一个问题
question = """
小明有 23 个苹果,小红比小明多 15 个。
小刚的苹果数是小明和小红总数的 2 倍。
请问:他们三人一共有多少个苹果?
"""

# 3、构造Prompt:明确要求"分步思考"
messages = [
    {
        "role": "system",
        "content": "你是一个数学解题高手。请务必分步思考,不要直接给出答案,列出每一步的计算过程。"
    },
    {
        "role": "user",
        "content": question
    }
]

# 4、发起请求
response = client.chat.completions.create(
    model=os.environ.get('LLM_MODEL_ID'),
    messages=messages,
)

# 5. 解析并展示
solution = response.choices[0].message.content
print(solution)

# solution = response.choices[0].message.parsed
# print("--- 🧠 思考过程 ---")
# for i, step in enumerate(solution.reasoning_steps, 1):
#     print(f"第{i}步: {step}")
# print("\n--- ✅ 最终答案 ---")
# print(solution.final_answer)

运行结果演示

先计算小红的苹果数:23 + 15 = 38。
小明和小红共有:23 + 38 = 61。
小刚的苹果数是他们总数的 2 倍:61 × 2 = 122。
三人一共有:23 + 38 + 122 = 183。
最终答案:183 个苹果。

这段代码的关键观察

• MathSolution 定义了一个理想的数据结构:reasoning_steps 是步骤列表,final_answer 是最终答案。

• 原始代码当前实际读取的是 response.choices[0].message.content,因此运行结果仍是字符串。

• 后半部分解析对象的代码被注释掉了,适合作为课堂练习:尝试把它与结构化输出连接起来。

• typing 中导入的 Literal 在这段代码中未实际使用,这是教学代码里常见的“预留/复用导入”。

为什么 List[str] 适合表达步骤

如果让模型输出一整段推理,它可能把多步计算混在一个句子里。把 reasoning_steps 设计成 List[str],等于要求每一步单独成项;这不仅方便展示,也方便后续做自动检查,例如检查每一步是否包含等式、是否跳步。

原始展示代码(保持不变)

Python
# 假设 solution 是上面解析出的对象

# 场景 A:显示详细推导(适合教育类 App)
print(f"推导过程:{'; '.join(solution.reasoning_steps)} -> 答案:{solution.final_answer}")

# 场景 B:只显示结果(适合工具类 App)
print(f"计算结果是:{solution.final_answer}")

# 场景 C:折叠显示(CLI 风格)
import click
click.echo(click.style("Thinking...", fg='yellow'))
# ... 展示推理 ...
click.echo(click.style(f"Done! Answer: {solution.final_answer}", fg='green'))

拓展案例:结构化地保存解题步骤

class StepByStepAnswer(BaseModel):
    steps: List[str] = Field(description="可展示给用户的解题步骤")
    answer: str = Field(description="最终答案")
    confidence: Literal["高", "中", "低"] = Field(description="模型对答案的信心")

# 设计意义:
# steps 用于教学展示,answer 用于最终反馈,confidence 用于决定是否需要人工复核。

这个变体把“解题步骤”“最终答案”“置信度”拆开。教育类 App 可以默认只显示 answer,用户点击“查看过程”时再展开 steps。

应用场景

• 数学解题:展示步骤,帮助学生定位哪一步没懂。

• 故障诊断:先列出可能原因,再给出排查顺序。

• 复杂决策:把方案比较拆成维度、权重和结论。

• 代码审查:先列出问题位置,再给出修复建议和风险说明。

三、四个任务的能力递进

本节学习定位

从“能调用”到“可编排”,从“字符串”到“对象”,这是模型层开发最重要的成长路径。

任务解决的问题核心手段产出形态
1 初次对话模型能否正常回复messages + SDK 请求自然语言字符串
2 Prompt 代码化输出格式不稳定System Prompt + Few-Shot + temperature=0JSON 字符串
3 结构化输出JSON 解析脆弱Pydantic + 字段约束Python 对象
4 分步推理复杂问题容易跳步步骤列表 + 最终答案可展示推导过程

一句话记忆

• Task 1 解决“怎么让模型说话”。

• Task 2 解决“怎么让模型按格式说话”。

• Task 3 解决“怎么让模型输出可被代码验证的数据”。

• Task 4 解决“怎么让模型面对复杂任务时分步输出”。

四、常见问题与排查清单

本节学习定位

初学阶段最容易卡在环境、密钥、模型 ID、输出格式四类问题上。遇到报错时,不要先怀疑模型,先按清单排查。

环境与鉴权

• ModuleNotFoundError: No module named openai:当前虚拟环境没有安装 openai,或运行脚本用的不是同一个解释器。

• AuthenticationError:API Key 缺失、过期、复制错误,或账户没有权限。

• 404 / model not found:LLM_MODEL_ID 写错,或该 base_url 下不存在这个模型。

• Connection error:网络、代理、base_url 或服务商接口地址异常。

输出与解析

• json.JSONDecodeError:模型输出不是纯 JSON,可能多了说明文字、Markdown 代码围栏或中文标点。

• KeyError: sentiment:JSON 虽然合法,但字段缺失;需要加强 Prompt 或使用结构化输出。

• AttributeError: parsed:当前 SDK/接口没有返回 parsed 对象;检查是否使用了支持结构化解析的调用方式。

• 同一输入多次输出不一致:降低 temperature,增加 Few-Shot,或把任务拆得更明确。

推荐调试顺序

  1. 先 print(os.environ.get("LLM_MODEL_ID")),确认环境变量读到了。

  2. 再用最小 messages 测试一次普通问答,排除鉴权和网络问题。

  3. 再打开 stream=False,直接打印 response,观察返回结构。

  4. 最后再加入 JSON、Pydantic、Few-Shot 等约束,逐步增加复杂度。

五、课后练习:把知识转成能力

本节学习定位

建议按顺序完成。每个练习都不要只看输出,要尝试解释为什么会得到这个输出。

  1. 把 Task 1 中的 system 改成“你是一个只用儿童语言解释概念的老师”,观察回答风格如何变化。

  2. 把 Task 2 的 real_user_input 替换成一条中性评论,例如“手机今天收到了,还没开始用”,观察 sentiment 是否稳定。

  3. 为 Task 2 增加一个字段 confidence,要求模型输出 0 到 1 的置信度。

  4. 在 Task 3 的 UserProfile 中增加 occupation 字段,并用 Field 描述提取规则。

  5. 把 Task 4 的数学题换成“鸡兔同笼”问题,检查模型是否能稳定分步。

  6. 尝试把 Task 4 的注释部分恢复为结构化解析,并比较 content 字符串与 parsed 对象的差异。

自测题

• 为什么多轮对话必须把历史 messages 重新发给模型?

• 为什么 temperature=0 更适合 JSON 解析任务?

• Few-Shot 示例越多越好吗?什么时候会因为示例过多导致上下文浪费?

• Field(description=...) 与普通注释最大的区别是什么?

• Literal 相比 str 能为业务系统带来什么好处?

六、参考与版本说明

• OpenAI 官方文档:Text generation / Chat Completions / Structured Outputs。

• 不同模型服务商对 OpenAI 兼容协议的支持程度不同,尤其是 response_format、parse、stream、tool calling 等高级特性。

• 本文件中的“拓展案例”均为新增教学示例,不替代原始代码;实际运行时请根据你的 SDK 版本和模型服务商文档调整。

学习建议

第一阶段的重点不是一次性掌握所有参数,而是养成“先跑通最小代码,再逐步增加格式约束,再把输出变成对象”的工程思维。