OpenAI API 实战指南

本文是我学习 OpenAI API 过程中的总结与梳理。在写完《Prompt 工程指南》后,我发现一个很实际的问题:Prompt 设计得再好,不知道怎么通过代码发给 LLM,不知道怎么处理返回结果,那终究是纸上谈兵。于是我开始啃 OpenAI 的官方文档,边学边写代码,把 Chat Completions、流式输出(SSE)、Function Calling、Token 计算、错误处理、多轮对话管理这些核心能力一个个摸了一遍。这篇文章就是我的学习笔记,每种技术都附上了我实际跑通的代码,还顺手搭了一个功能完整的 AI 对话应用来验证所学。

声明:本文为作者在学习 过程中的总结与梳理,仅供学习参考。由于作者水平有限,文中可能存在表述不准确或遗漏之处,欢迎读者提出指正与交流。配套示例项目 github: ai-chat-app/ gitee: ai-chat-app 中找到并直接运行。


目录

  1. 引言:从 Prompt 到 API —— 理论到实践的桥梁
  2. 环境准备与认证配置
  3. Chat Completions API 深度解析
  4. 流式输出:原理与实现
  5. Function Calling:让 LLM 使用工具
  6. Token 计算与成本控制
  7. 错误处理与重试策略
  8. 多轮对话管理
  9. 结构化输出
  10. 生产环境最佳实践
  11. 配套项目:AI 对话应用
  12. 总结与建议

1. 引言:从 Prompt 到 API —— 理论到实践的桥梁

1.1 为什么要写这篇文章

在写完《Prompt 工程指南》之后,我掌握了五要素结构、五大推理技术、分层 Prompt 架构这些设计层面的知识。但很快我就发现一个问题:我知道怎么写一个好的 Prompt,但我不知道怎么通过代码把它发给 LLM,不知道怎么处理返回结果,遇到报错也不知道该怎么办。

于是我开始动手写代码,从最简单的 Chat Completions 调用开始,一步步摸索流式输出、Function Calling、Token 计算、错误处理……这篇文章记录的就是我这一路踩坑和收获的过程。

下面这张图是我理解的两层关系——Prompt 工程是"设计层",OpenAI API 是"实现层":

Prompt 工程(设计层)          OpenAI API(实现层)
      │                              │
      ├─ 五要素结构          →       ├─ messages 参数构建
      ├─ Few-shot / CoT     →       ├─ 多轮对话中的 Prompt 注入
      ├─ 结构化输出          →       ├─ response_format / Function Calling
      ├─ 分层 Prompt 架构    →       ├─ System / User / Assistant 消息分层
      ├─ A/B 测试           →       ├─ 多模型并行调用对比
      └─ 安全防护            →       └─ 输入过滤 + 输出审核

1.2 我学到了什么

经过这段时间的学习和实践,我掌握了以下能力:

  • 独立搭建一个支持流式输出的 AI 对话应用
  • 实现 Function Calling,让 LLM 调用外部 API 和工具
  • 精确计算 Token 消耗,控制 API 成本
  • 处理各种 API 错误(限流、超时、内容过滤等)
  • 管理多轮对话的上下文窗口
  • 将 Prompt 工程的最佳实践落地到生产代码中

1.3 我的学习体会

回头看这段学习经历,有几个感受想分享:

  • 流式输出是体验的分水岭,加了之后整个应用的感觉完全不一样了,从"等结果"变成了"看结果"
  • Function Calling 是最让我兴奋的部分,它让 LLM 从"聊天机器人"变成了真正能"干活"的助手
  • 错误处理一开始觉得繁琐,但实际跑起来才发现,网络波动、限流、超时这些情况太常见了,不处理根本没法用
  • 如果你是刚开始学,建议按顺序来,每节的代码我都验证过可以直接跑;如果已经有基础,可以直接跳到流式输出和 Function Calling 这两节,这是我觉得最有意思的部分

2. 环境准备与认证配置

2.1 安装依赖

pip install openai tiktoken streamlit python-dotenv

2.2 API Key 配置

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

class APIConfig:
    """OpenAI API 配置"""

    # 从环境变量读取,避免硬编码
    API_KEY = os.getenv("OPENAI_API_KEY")
    BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")

    # 模型配置
    DEFAULT_MODEL = "gpt-4o-mini"       # 性价比最高的默认模型
    REASONING_MODEL = "gpt-4o"          # 复杂推理任务
    FAST_MODEL = "gpt-4o-mini"          # 简单快速任务

    # 请求配置
    DEFAULT_TEMPERATURE = 0.7
    DEFAULT_MAX_TOKENS = 2048
    REQUEST_TIMEOUT = 60                 # 请求超时(秒)

    # 重试配置
    MAX_RETRIES = 3
    RETRY_BACKOFF_FACTOR = 2            # 指数退避因子
    RETRY_INITIAL_DELAY = 1             # 初始延迟(秒)

    # 限流配置
    MAX_REQUESTS_PER_MINUTE = 500       # RPM 限制(取决于你的 Tier)
    MAX_TOKENS_PER_MINUTE = 200000      # TPM 限制

    @classmethod
    def validate(cls):
        """验证配置是否完整"""
        if not cls.API_KEY:
            raise ValueError(
                "未找到 OPENAI_API_KEY,请在 .env 文件中设置或设置环境变量"
            )
        if cls.API_KEY == "sk-your-api-key-here":
            raise ValueError("请将 OPENAI_API_KEY 替换为你的真实 API Key")

2.3 .env 文件

# .env
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_BASE_URL=https://api.openai.com/v1

2.4 客户端初始化

# llm_client.py - 基础客户端
from openai import OpenAI
from config import APIConfig


def create_client() -> OpenAI:
    """创建 OpenAI 客户端实例"""
    APIConfig.validate()
    return OpenAI(
        api_key=APIConfig.API_KEY,
        base_url=APIConfig.BASE_URL,
        timeout=APIConfig.REQUEST_TIMEOUT,
        max_retries=0,  # 我们自己管理重试逻辑
    )

3. Chat Completions API 深度解析

3.1 消息角色体系

OpenAI Chat API 用结构化的消息列表来组织对话,这是我刚开始接触时觉得最巧妙的设计。三种消息角色各司其职:

┌─────────────────────────────────────────────────────────┐
│                    消息角色体系                           │
│                                                         │
│  system    : 系统级指令,定义 AI 的行为边界和角色          │
│              → 对应 Prompt 工程中的"角色设定"+"约束条件"    │
│              → 权重最高,AI 会优先遵循                     │
│                                                         │
│  user      : 用户输入,代表人类的问题或指令                 │
│              → 对应 Prompt 工程中的"任务指令"+"上下文"      │
│                                                         │
│  assistant : AI 的历史回复,用于维持对话连贯性              │
│              → 在多轮对话中,需要把历史 assistant 消息传回   │
│                                                         │
│  tool      : 工具调用的结果,Function Calling 专用          │
│              → AI 调用工具后,结果以 tool 角色传回          │
└─────────────────────────────────────────────────────────┘
from openai import OpenAI

client = create_client()

# 基础调用示例
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "system",
            "content": "你是一位Python技术专家。回答要包含可运行的代码示例。"
        },
        {
            "role": "user",
            "content": "如何用Python读取CSV文件并计算某一列的平均值?"
        }
    ],
    temperature=0.7,
    max_tokens=1000,
)

# 提取回复内容
answer = response.choices[0].message.content
print(answer)

# 查看 Token 使用情况
print(f"输入 Token: {response.usage.prompt_tokens}")
print(f"输出 Token: {response.usage.completion_tokens}")
print(f"总计 Token: {response.usage.total_tokens}")

3.2 核心参数详解

参数类型默认值说明
modelstr必填模型名称,如 gpt-4o、gpt-4o-mini
messageslist必填消息列表,按对话顺序排列
temperaturefloat1.0控制随机性,0=确定,2=最大随机
max_tokensint无限制输出 Token 上限(不含输入)
top_pfloat1.0核采样,与 temperature 二选一即可
nint1生成多少个候选回复
streamboolFalse是否启用流式输出
stopstr/listNone停止生成的标记
presence_penaltyfloat0-2.0~2.0,正值鼓励谈论新话题
frequency_penaltyfloat0-2.0~2.0,正值减少重复
response_formatdictNone{"type": "json_object"} 强制 JSON
toolslistNoneFunction Calling 工具定义
tool_choicestr/dict“auto”工具选择策略
Temperature 选择指南
temperature 值的选择取决于任务类型:

0.0 - 0.2  → 事实性问答、代码生成、数学计算、JSON 输出
             特点:输出稳定、可复现,但缺乏创造性

0.3 - 0.5  → 技术写作、翻译、摘要、数据分析
             特点:在准确性和灵活性之间取得平衡

0.6 - 0.8  → 创意写作、头脑风暴、对话、角色扮演
             特点:输出多样、有创意,但可能不够精确

0.9 - 1.5  → 诗歌创作、故事生成、探索性任务
             特点:高度随机,输出不可预测

3.3 响应结构解析

# 完整的响应对象结构
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "你好"}],
)

# response 对象的关键属性
print(f"响应ID: {response.id}")
print(f"模型: {response.model}")
print(f"创建时间: {response.created}")

# choices 列表(n=1 时通常只有一个)
choice = response.choices[0]
print(f"结束原因: {choice.finish_reason}")
# finish_reason 可能的值:
# - "stop": 正常结束
# - "length": 达到 max_tokens 限制
# - "content_filter": 内容被过滤
# - "tool_calls": 模型请求调用工具
# - "function_call": 模型请求调用函数(旧版)

print(f"消息角色: {choice.message.role}")
print(f"回复内容: {choice.message.content}")
print(f"工具调用: {choice.message.tool_calls}")

# usage 信息
print(f"输入Token: {response.usage.prompt_tokens}")
print(f"输出Token: {response.usage.completion_tokens}")
print(f"总计Token: {response.usage.total_tokens}")

4. 流式输出:原理与实现

4.1 SSE 协议原理

流式输出是我觉得最能提升用户体验的一个特性。它基于 SSE(Server-Sent Events) 协议,原理其实不复杂:

传统请求-响应模式:
客户端 ──请求──→ 服务器 ──等待LLM生成完毕──→ 客户端
         (等待 3-10 秒,用户看到空白)

流式输出模式:
客户端 ──请求──→ 服务器 ──token1──→ 客户端(显示"你好")
                         ──token2──→ 客户端(显示"你好,我")
                         ──token3──→ 客户端(显示"你好,我是")
                         ──token4──→ 客户端(显示"你好,我是AI")
                         ...
                         ──[DONE]──→ 客户端(完成)
         (用户看到文字逐字出现,体验流畅)

SSE 数据格式:

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"你好"}}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":",我"}}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"是"}}]}

data: [DONE]

4.2 Python 流式客户端实现

# llm_client.py - 流式客户端
from openai import OpenAI
from openai.types.chat import ChatCompletionChunk
from typing import Generator


class StreamingClient:
    """支持流式输出的 OpenAI 客户端"""

    def __init__(self, client: OpenAI, model: str = "gpt-4o-mini"):
        self.client = client
        self.model = model

    def chat_stream(
        self,
        messages: list[dict],
        temperature: float = 0.7,
        max_tokens: int = 2048,
    ) -> Generator[str, None, None]:
        """
        流式对话,逐 token 返回

        Yields:
            str: 每次返回一个增量文本片段
        """
        stream = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens,
            stream=True,
            stream_options={"include_usage": True},  # 获取最终 usage 信息
        )

        for chunk in stream:
            # 检查是否有内容
            if chunk.choices and len(chunk.choices) > 0:
                delta = chunk.choices[0].delta
                if delta.content:
                    yield delta.content

    def chat_stream_with_usage(
        self,
        messages: list[dict],
        temperature: float = 0.7,
        max_tokens: int = 2048,
    ) -> Generator[dict, None, None]:
        """
        流式对话,返回内容和 usage 信息

        Yields:
            dict: {"type": "content", "data": "文本"} 或
                  {"type": "usage", "data": {...}}
        """
        stream = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens,
            stream=True,
            stream_options={"include_usage": True},
        )

        for chunk in stream:
            if chunk.choices and len(chunk.choices) > 0:
                delta = chunk.choices[0].delta
                if delta.content:
                    yield {"type": "content", "data": delta.content}

            # 最后一个 chunk 包含 usage 信息
            if hasattr(chunk, "usage") and chunk.usage:
                yield {
                    "type": "usage",
                    "data": {
                        "prompt_tokens": chunk.usage.prompt_tokens,
                        "completion_tokens": chunk.usage.completion_tokens,
                        "total_tokens": chunk.usage.total_tokens,
                    }
                }


# 使用示例
if __name__ == "__main__":
    client = create_client()
    stream_client = StreamingClient(client)

    messages = [
        {"role": "system", "content": "你是一位Python技术专家。"},
        {"role": "user", "content": "用一句话解释什么是装饰器。"},
    ]

    print("AI 回复: ", end="", flush=True)
    for chunk in stream_client.chat_stream(messages):
        print(chunk, end="", flush=True)
    print()

4.3 前端流式渲染

前端接收流式输出,我了解到通常用 EventSource 或 Fetch API + ReadableStream:

// 前端流式接收示例(JavaScript)
async function chatWithStream(userMessage) {
    const response = await fetch('/api/chat/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: userMessage }),
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        // 解析 SSE 数据
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';  // 保留不完整的行

        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const data = line.slice(6);
                if (data === '[DONE]') return;

                try {
                    const parsed = JSON.parse(data);
                    const content = parsed.choices?.[0]?.delta?.content;
                    if (content) {
                        // 逐字追加到页面
                        appendToChat(content);
                    }
                } catch (e) {
                    // 忽略解析错误
                }
            }
        }
    }
}

5. Function Calling:让 LLM 使用工具

5.1 Function Calling 原理

Function Calling 是我学习过程中最兴奋的发现。它让 LLM 能够"意识到"自己需要外部工具,并生成结构化的工具调用请求。刚开始我以为 LLM 真的会去"执行"函数,后来才搞明白:

核心流程:

用户: "今天北京天气怎么样?"
        │
        ▼
┌──────────────────┐
│   LLM 分析意图    │  → "我需要查询天气,但我没有实时数据"
│   决定调用工具     │  → "应该调用 get_weather 函数"
└──────┬───────────┘
       │ 返回 tool_calls(不是文本回复)
       ▼
┌──────────────────┐
│   你的代码执行     │  → 调用天气 API,获取真实数据
│   获取工具结果     │  → {"temperature": 22, "condition": "晴"}
└──────┬───────────┘
       │ 将结果以 tool 角色传回
       ▼
┌──────────────────┐
│   LLM 整合信息    │  → "北京今天晴天,气温22°C,适合户外活动"
│   生成最终回复     │
└──────────────────┘

我的理解:Function Calling 不是 LLM 真的"执行"了函数,而是 LLM 告诉你"我想调用哪个函数、传什么参数",由你的代码来实际执行。这个设计很巧妙——LLM 负责"决策",代码负责"执行",各司其职。

5.2 工具定义与注册

# function_tools.py
import json
import math
from typing import Any
from datetime import datetime


# ============================================================
# 工具定义(JSON Schema 格式)
# ============================================================

TOOL_DEFINITIONS = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "获取指定城市的当前天气信息。当用户询问天气相关问题时使用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如'北京'、'上海'、'Tokyo'",
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位,celsius(摄氏度)或 fahrenheit(华氏度)",
                    },
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "执行数学计算。支持基本运算、三角函数、对数等。当用户需要进行数学计算时使用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式,如'2+3*4'、'sqrt(16)'、'sin(pi/2)'",
                    },
                },
                "required": ["expression"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "在内部知识库中搜索信息。当用户询问需要专业知识或内部文档的问题时使用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索查询词",
                    },
                    "top_k": {
                        "type": "integer",
                        "description": "返回结果数量,默认3",
                        "default": 3,
                    },
                },
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "获取当前日期和时间。当用户询问当前时间、日期时使用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "timezone": {
                        "type": "string",
                        "description": "时区,如'Asia/Shanghai'、'America/New_York',默认为'Asia/Shanghai'",
                    },
                },
            },
        },
    },
]


# ============================================================
# 工具实现
# ============================================================

class ToolExecutor:
    """工具执行器"""

    def __init__(self):
        # 注册工具名称到实现函数的映射
        self._tools = {
            "get_current_weather": self._get_weather,
            "calculate": self._calculate,
            "search_knowledge_base": self._search_kb,
            "get_current_time": self._get_time,
        }

    def execute(self, tool_name: str, arguments: dict) -> str:
        """执行指定的工具"""
        if tool_name not in self._tools:
            return json.dumps({"error": f"未知工具: {tool_name}"})

        try:
            result = self._tools[tool_name](**arguments)
            return json.dumps(result, ensure_ascii=False)
        except Exception as e:
            return json.dumps({"error": str(e)}, ensure_ascii=False)

    def _get_weather(self, city: str, unit: str = "celsius") -> dict:
        """获取天气(模拟数据,实际应调用天气API)"""
        # 实际项目中替换为真实天气 API 调用
        # 例如:requests.get(f"https://api.weather.com/v1/current?city={city}")
        weather_data = {
            "北京": {"temperature": 22, "condition": "晴", "humidity": 45},
            "上海": {"temperature": 26, "condition": "多云", "humidity": 65},
            "广州": {"temperature": 30, "condition": "阵雨", "humidity": 80},
            "深圳": {"temperature": 29, "condition": "阴", "humidity": 75},
            "Tokyo": {"temperature": 20, "condition": "晴", "humidity": 50},
        }

        default = {"temperature": 18, "condition": "未知", "humidity": 60}
        weather = weather_data.get(city, default)

        if unit == "fahrenheit":
            weather["temperature"] = round(weather["temperature"] * 9 / 5 + 32, 1)

        return {
            "city": city,
            "unit": unit,
            **weather,
            "timestamp": datetime.now().isoformat(),
        }

    def _calculate(self, expression: str) -> dict:
        """安全地执行数学计算"""
        # 白名单:只允许安全的数学函数
        allowed_names = {
            "abs": abs, "round": round, "min": min, "max": max,
            "sum": sum, "pow": pow,
            "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
            "tan": math.tan, "log": math.log, "log10": math.log10,
            "pi": math.pi, "e": math.e,
        }

        try:
            # 编译表达式(安全沙箱)
            code = compile(expression, "<calc>", "eval")

            # 检查是否使用了未允许的名称
            for name in code.co_names:
                if name not in allowed_names:
                    raise ValueError(f"不允许的函数或变量: {name}")

            result = eval(code, {"__builtins__": {}}, allowed_names)
            return {"expression": expression, "result": result}
        except Exception as e:
            return {"expression": expression, "error": str(e)}

    def _search_kb(self, query: str, top_k: int = 3) -> dict:
        """搜索知识库(模拟数据)"""
        # 实际项目中替换为向量数据库搜索
        kb = {
            "python": "Python 是一种解释型、面向对象的高级编程语言,由 Guido van Rossum 于 1991 年发布。",
            "openai": "OpenAI 是一家人工智能研究公司,开发了 GPT 系列大语言模型。其 API 提供了文本生成、图像生成、语音识别等服务。",
            "rag": "RAG(检索增强生成)是一种结合信息检索和文本生成的 AI 技术架构,通过先检索相关文档再生成回答来提高准确性。",
            "prompt": "Prompt 工程是设计和优化输入给大语言模型的提示文本的技术,目的是获得更准确、更有用的输出。",
        }

        results = []
        for key, value in kb.items():
            if query.lower() in key.lower() or query.lower() in value.lower():
                results.append({"source": key, "content": value})

        return {
            "query": query,
            "results": results[:top_k],
            "total_found": len(results),
        }

    def _get_time(self, timezone: str = "Asia/Shanghai") -> dict:
        """获取当前时间"""
        now = datetime.now()
        return {
            "datetime": now.isoformat(),
            "date": now.strftime("%Y-%m-%d"),
            "time": now.strftime("%H:%M:%S"),
            "weekday": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][now.weekday()],
            "timezone": timezone,
        }

5.3 完整调用流程

# llm_client.py - Function Calling 客户端
import json
from openai import OpenAI
from function_tools import TOOL_DEFINITIONS, ToolExecutor


class FunctionCallingClient:
    """支持 Function Calling 的客户端"""

    def __init__(self, client: OpenAI, model: str = "gpt-4o-mini"):
        self.client = client
        self.model = model
        self.tool_executor = ToolExecutor()

    def chat_with_tools(
        self,
        messages: list[dict],
        tools: list[dict] = None,
        temperature: float = 0.7,
        max_iterations: int = 5,
    ) -> dict:
        """
        支持工具调用的对话

        会自动处理多轮工具调用,直到 LLM 不再请求工具或达到最大迭代次数。

        Returns:
            dict: {
                "answer": "最终回复文本",
                "tool_calls_made": [...],  # 所有工具调用记录
                "total_tokens": 1234,
            }
        """
        if tools is None:
            tools = TOOL_DEFINITIONS

        tool_calls_made = []
        total_tokens = 0

        for iteration in range(max_iterations):
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                tools=tools,
                tool_choice="auto",  # 让模型自己决定是否调用工具
                temperature=temperature,
            )

            choice = response.choices[0]
            total_tokens += response.usage.total_tokens

            # 情况1:模型想调用工具
            if choice.finish_reason == "tool_calls" and choice.message.tool_calls:
                # 将模型的工具调用请求加入消息历史
                messages.append({
                    "role": "assistant",
                    "content": choice.message.content,
                    "tool_calls": [
                        {
                            "id": tc.id,
                            "type": "function",
                            "function": {
                                "name": tc.function.name,
                                "arguments": tc.function.arguments,
                            },
                        }
                        for tc in choice.message.tool_calls
                    ],
                })

                # 执行每个工具调用
                for tc in choice.message.tool_calls:
                    tool_name = tc.function.name
                    arguments = json.loads(tc.function.arguments)

                    # 执行工具
                    result = self.tool_executor.execute(tool_name, arguments)

                    tool_calls_made.append({
                        "tool": tool_name,
                        "arguments": arguments,
                        "result": result,
                    })

                    # 将工具结果加入消息历史
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc.id,
                        "content": result,
                    })

                # 继续循环,让模型处理工具结果
                continue

            # 情况2:模型直接回复文本
            if choice.message.content:
                return {
                    "answer": choice.message.content,
                    "tool_calls_made": tool_calls_made,
                    "total_tokens": total_tokens,
                }

            # 情况3:异常情况
            return {
                "answer": "抱歉,我无法处理这个请求。",
                "tool_calls_made": tool_calls_made,
                "total_tokens": total_tokens,
            }

        # 达到最大迭代次数
        return {
            "answer": "处理超时,请简化您的问题后重试。",
            "tool_calls_made": tool_calls_made,
            "total_tokens": total_tokens,
        }


# 使用示例
if __name__ == "__main__":
    client = create_client()
    fc_client = FunctionCallingClient(client)

    messages = [
        {"role": "system", "content": "你是一个有用的助手,可以使用工具来回答问题。"},
        {"role": "user", "content": "北京今天天气怎么样?顺便帮我算一下 123 * 456 等于多少。"},
    ]

    result = fc_client.chat_with_tools(messages)
    print(f"回复: {result['answer']}")
    print(f"工具调用: {json.dumps(result['tool_calls_made'], ensure_ascii=False, indent=2)}")
    print(f"Token 消耗: {result['total_tokens']}")

5.4 与 Prompt 工程的结合

Function Calling 不是 Prompt 工程的替代,而是增强。回顾《Prompt 工程指南》中学到的内容,我尝试把两者结合起来:

# prompt_templates.py - 结合 Prompt 工程的 Function Calling
from function_tools import TOOL_DEFINITIONS


class PromptEnhancedToolCalling:
    """结合 Prompt 工程最佳实践的 Function Calling"""

    @staticmethod
    def build_system_prompt_with_tools(
        role: str,
        task: str,
        rules: list[str] = None,
        tool_usage_guide: str = None,
    ) -> str:
        """
        构建包含工具使用指导的 System Prompt

        对应 Prompt 工程中的"分层 Prompt 架构":
        Layer 1(系统层):角色 + 规则 + 工具使用指导
        """
        prompt = f"你是一位{role}。\n\n"

        prompt += f"【核心任务】\n{task}\n\n"

        if rules:
            prompt += "【行为规则】\n"
            for i, rule in enumerate(rules, 1):
                prompt += f"{i}. {rule}\n"
            prompt += "\n"

        if tool_usage_guide:
            prompt += f"【工具使用指导】\n{tool_usage_guide}\n\n"
        else:
            prompt += """【工具使用指导】
- 当需要实时数据(天气、时间)或精确计算时,使用工具获取
- 一次可以调用多个工具,工具调用是并行的
- 获取工具结果后,用自然语言整合信息回复用户
- 如果工具返回错误,告知用户并尝试其他方式
"""

        return prompt

    @staticmethod
    def build_few_shot_tool_examples() -> list[dict]:
        """
        构建 Few-shot 工具调用示例

        对应 Prompt 工程中的"少样本提示(Few-shot Prompting)"
        通过示例教会模型何时使用工具、如何使用
        """
        return [
            {"role": "user", "content": "上海今天热吗?"},
            {
                "role": "assistant",
                "content": None,
                "tool_calls": [{
                    "id": "call_example_1",
                    "type": "function",
                    "function": {
                        "name": "get_current_weather",
                        "arguments": '{"city": "上海", "unit": "celsius"}',
                    },
                }],
            },
            {
                "role": "tool",
                "tool_call_id": "call_example_1",
                "content": '{"city": "上海", "temperature": 26, "condition": "多云", "humidity": 65}',
            },
            {
                "role": "assistant",
                "content": "上海今天多云,气温26°C,湿度65%。体感偏热,建议穿短袖,出门注意防晒。",
            },
        ]

    @staticmethod
    def build_cot_tool_prompt(question: str) -> str:
        """
        构建 CoT + 工具调用的 Prompt

        对应 Prompt 工程中的"思维链提示(Chain-of-Thought)"
        引导模型先思考需要什么信息,再决定调用什么工具
        """
        return f"""请按以下步骤思考并回答:

步骤1:分析问题,确定需要哪些信息
步骤2:判断哪些信息可以通过工具获取
步骤3:调用相应的工具
步骤4:整合工具返回的信息,给出最终答案

问题:{question}

请开始分析:"""

6. Token 计算与成本控制

6.1 Token 计算原理

Token 是 LLM 处理文本的基本单位。刚开始我以为是按字符或单词算的,后来才发现完全不是那么回事。搞清楚 Token 怎么算,是控制成本的第一步:

Token 不等于字符,也不等于单词:

英文:1 token ≈ 0.75 个单词 ≈ 4 个字符
      "Hello, world!" → 4 tokens

中文:1 token ≈ 0.5-1 个汉字
      "你好世界" → 4-6 tokens(取决于模型的分词器)

代码:1 token ≈ 3-4 个字符
      "def foo():" → 4 tokens
# token_tracker.py
import tiktoken
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class TokenUsage:
    """Token 使用记录"""
    prompt_tokens: int = 0
    completion_tokens: int = 0
    total_tokens: int = 0

    def __add__(self, other: "TokenUsage") -> "TokenUsage":
        return TokenUsage(
            prompt_tokens=self.prompt_tokens + other.prompt_tokens,
            completion_tokens=self.completion_tokens + other.completion_tokens,
            total_tokens=self.total_tokens + other.total_tokens,
        )


class TokenCalculator:
    """Token 计算器"""

    # 各模型的价格(每百万 Token,美元)
    # 价格可能变动,请以 OpenAI 官网为准
    PRICING = {
        "gpt-4o": {"input": 2.50, "output": 10.00},
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
        "gpt-4-turbo": {"input": 10.00, "output": 30.00},
        "gpt-4": {"input": 30.00, "output": 60.00},
        "gpt-3.5-turbo": {"input": 0.50, "output": 1.50},
    }

    def __init__(self, model: str = "gpt-4o-mini"):
        self.model = model
        try:
            self.encoding = tiktoken.encoding_for_model(model)
        except KeyError:
            self.encoding = tiktoken.get_encoding("cl100k_base")

    def count_tokens(self, text: str) -> int:
        """计算文本的 Token 数量"""
        return len(self.encoding.encode(text))

    def count_messages_tokens(self, messages: list[dict]) -> int:
        """
        计算消息列表的 Token 数量

        参考 OpenAI 的计数规则:
        - 每条消息有固定的开销(约 3-4 tokens)
        - 角色名称计入 token
        - 工具定义也计入 token
        """
        tokens_per_message = 3  # 每条消息的固定开销
        num_tokens = 0

        for message in messages:
            num_tokens += tokens_per_message
            for key, value in message.items():
                if key == "content" and value:
                    num_tokens += self.count_tokens(str(value))
                elif key == "tool_calls":
                    # 工具调用也计入 token
                    num_tokens += self.count_tokens(str(value))
                elif key == "name":
                    num_tokens += self.count_tokens(str(value))

        num_tokens += 3  # 每次回复的固定开销
        return num_tokens

    def estimate_cost(
        self,
        prompt_tokens: int,
        completion_tokens: int,
        model: str = None,
    ) -> dict:
        """估算 API 调用成本"""
        model = model or self.model
        pricing = self.PRICING.get(model, {"input": 0, "output": 0})

        input_cost = (prompt_tokens / 1_000_000) * pricing["input"]
        output_cost = (completion_tokens / 1_000_000) * pricing["output"]
        total_cost = input_cost + output_cost

        return {
            "model": model,
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "input_cost_usd": round(input_cost, 6),
            "output_cost_usd": round(output_cost, 6),
            "total_cost_usd": round(total_cost, 6),
            "total_cost_cny": round(total_cost * 7.2, 6),  # 粗略汇率
        }


class TokenTracker:
    """Token 使用追踪器(用于统计整个会话的成本)"""

    def __init__(self, model: str = "gpt-4o-mini"):
        self.model = model
        self.calculator = TokenCalculator(model)
        self.usage = TokenUsage()
        self.call_history: list[dict] = []

    def record(self, prompt_tokens: int, completion_tokens: int):
        """记录一次 API 调用的 Token 使用"""
        self.usage.prompt_tokens += prompt_tokens
        self.usage.completion_tokens += completion_tokens
        self.usage.total_tokens += prompt_tokens + completion_tokens

        cost = self.calculator.estimate_cost(prompt_tokens, completion_tokens)
        self.call_history.append(cost)

    def get_summary(self) -> dict:
        """获取使用摘要"""
        total_cost = self.calculator.estimate_cost(
            self.usage.prompt_tokens,
            self.usage.completion_tokens,
        )
        return {
            "model": self.model,
            "total_calls": len(self.call_history),
            "total_prompt_tokens": self.usage.prompt_tokens,
            "total_completion_tokens": self.usage.completion_tokens,
            "total_tokens": self.usage.total_tokens,
            "total_cost_usd": total_cost["total_cost_usd"],
            "total_cost_cny": total_cost["total_cost_cny"],
            "call_history": self.call_history,
        }


# 使用示例
if __name__ == "__main__":
    calc = TokenCalculator("gpt-4o-mini")

    text = "Prompt 工程是设计和优化输入给大语言模型的提示文本的技术。"
    tokens = calc.count_tokens(text)
    print(f"文本: {text}")
    print(f"Token 数: {tokens}")

    messages = [
        {"role": "system", "content": "你是一个有用的助手。"},
        {"role": "user", "content": "解释什么是 RAG。"},
    ]
    msg_tokens = calc.count_messages_tokens(messages)
    print(f"消息 Token 数: {msg_tokens}")

    cost = calc.estimate_cost(500, 200)
    print(f"预估成本: ${cost['total_cost_usd']} (约 {cost['total_cost_cny']} 元)")

6.2 成本估算与优化

# 成本优化策略汇总

class CostOptimizer:
    """API 成本优化器"""

    @staticmethod
    def select_model(task_complexity: str) -> str:
        """
        根据任务复杂度选择模型

        简单任务用便宜模型,复杂任务用强模型
        """
        model_map = {
            "simple": "gpt-4o-mini",      # 简单问答、分类 → $0.15/M input
            "moderate": "gpt-4o-mini",    # 摘要、翻译 → $0.15/M input
            "complex": "gpt-4o",          # 复杂推理、代码生成 → $2.50/M input
            "critical": "gpt-4o",         # 关键业务 → $2.50/M input
        }
        return model_map.get(task_complexity, "gpt-4o-mini")

    @staticmethod
    def optimize_messages(
        messages: list[dict],
        max_context_tokens: int = 4000,
    ) -> list[dict]:
        """
        优化消息列表,控制上下文长度

        策略:
        1. System Prompt 始终保留
        2. 保留最近的 N 轮对话
        3. 超出部分用摘要替代
        """
        if len(messages) <= 3:
            return messages

        calc = TokenCalculator()
        system_msgs = [m for m in messages if m["role"] == "system"]
        other_msgs = [m for m in messages if m["role"] != "system"]

        # 从后往前保留,直到接近 token 限制
        kept = []
        current_tokens = sum(calc.count_tokens(m.get("content", "")) for m in system_msgs)

        for msg in reversed(other_msgs):
            msg_tokens = calc.count_tokens(msg.get("content", ""))
            if current_tokens + msg_tokens > max_context_tokens:
                break
            kept.insert(0, msg)
            current_tokens += msg_tokens

        return system_msgs + kept

    @staticmethod
    def estimate_conversation_cost(
        avg_input_tokens: int,
        avg_output_tokens: int,
        conversations_per_day: int,
        model: str = "gpt-4o-mini",
    ) -> dict:
        """估算每日/每月成本"""
        calc = TokenCalculator(model)
        daily_input = avg_input_tokens * conversations_per_day
        daily_output = avg_output_tokens * conversations_per_day

        daily_cost = calc.estimate_cost(daily_input, daily_output)
        monthly_cost = calc.estimate_cost(daily_input * 30, daily_output * 30)

        return {
            "model": model,
            "conversations_per_day": conversations_per_day,
            "daily_cost_usd": daily_cost["total_cost_usd"],
            "daily_cost_cny": daily_cost["total_cost_cny"],
            "monthly_cost_usd": monthly_cost["total_cost_usd"],
            "monthly_cost_cny": monthly_cost["total_cost_cny"],
        }


# 使用示例
optimizer = CostOptimizer()
cost_estimate = optimizer.estimate_conversation_cost(
    avg_input_tokens=500,
    avg_output_tokens=300,
    conversations_per_day=100,
    model="gpt-4o-mini",
)
print(f"使用 gpt-4o-mini,每天 100 次对话:")
print(f"  每日成本: ${cost_estimate['daily_cost_usd']:.4f} (约 {cost_estimate['daily_cost_cny']:.2f} 元)")
print(f"  每月成本: ${cost_estimate['monthly_cost_usd']:.4f} (约 {cost_estimate['monthly_cost_cny']:.2f} 元)")

7. 错误处理与重试策略

7.1 错误类型分类

实际跑起来之后,我发现 API 调用出错的概率比想象中高得多。网络波动、限流、超时、内容过滤……各种情况都遇到过。我把常见的错误整理成了下面这张表:

错误类型HTTP 状态码典型场景是否可重试
RateLimitError429超过 RPM/TPM 限制是(等待后重试)
APIConnectionError-网络问题、DNS 解析失败是
APITimeoutError-请求超时是
InternalServerError500OpenAI 服务器内部错误是
ServiceUnavailableError503服务暂时不可用是
AuthenticationError401API Key 无效或过期否
PermissionDeniedError403无权限访问该资源否
InvalidRequestError400请求参数错误否(修正参数后重试)
ContentFilterError400内容被安全过滤否

7.2 指数退避重试

# llm_client.py - 带重试的客户端
import time
import random
from functools import wraps
from openai import (
    OpenAI,
    RateLimitError,
    APIConnectionError,
    APITimeoutError,
    InternalServerError,
    APIStatusError,
)


class RetryConfig:
    """重试配置"""

    def __init__(
        self,
        max_retries: int = 3,
        initial_delay: float = 1.0,
        backoff_factor: float = 2.0,
        max_delay: float = 60.0,
        jitter: bool = True,
        retryable_errors: tuple = (
            RateLimitError,
            APIConnectionError,
            APITimeoutError,
            InternalServerError,
        ),
    ):
        self.max_retries = max_retries
        self.initial_delay = initial_delay
        self.backoff_factor = backoff_factor
        self.max_delay = max_delay
        self.jitter = jitter
        self.retryable_errors = retryable_errors

    def get_delay(self, attempt: int) -> float:
        """计算第 N 次重试的延迟时间(指数退避)"""
        delay = self.initial_delay * (self.backoff_factor ** attempt)
        delay = min(delay, self.max_delay)

        if self.jitter:
            # 添加随机抖动,避免"惊群效应"
            delay = delay * (0.5 + random.random())

        return delay


def with_retry(retry_config: RetryConfig = None):
    """重试装饰器"""
    if retry_config is None:
        retry_config = RetryConfig()

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None

            for attempt in range(retry_config.max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except retry_config.retryable_errors as e:
                    last_error = e

                    if attempt == retry_config.max_retries:
                        # 达到最大重试次数,抛出最终错误
                        raise RuntimeError(
                            f"API 调用失败,已重试 {retry_config.max_retries} 次。"
                            f"最后错误: {type(e).__name__}: {str(e)[:200]}"
                        ) from e

                    delay = retry_config.get_delay(attempt)
                    print(
                        f"[重试 {attempt + 1}/{retry_config.max_retries}] "
                        f"{type(e).__name__},等待 {delay:.1f} 秒..."
                    )
                    time.sleep(delay)

                except APIStatusError as e:
                    # 非可重试错误,直接抛出
                    raise RuntimeError(
                        f"API 返回错误 (HTTP {e.status_code}): {str(e)[:500]}"
                    ) from e

            # 理论上不会到这里
            raise last_error

        return wrapper
    return decorator


class RobustClient:
    """带重试和错误处理的 OpenAI 客户端"""

    def __init__(
        self,
        client: OpenAI,
        model: str = "gpt-4o-mini",
        retry_config: RetryConfig = None,
    ):
        self.client = client
        self.model = model
        self.retry_config = retry_config or RetryConfig()

    def chat(
        self,
        messages: list[dict],
        temperature: float = 0.7,
        max_tokens: int = 2048,
        **kwargs,
    ) -> dict:
        """
        带重试的对话接口

        Returns:
            dict: {
                "success": True/False,
                "content": "回复内容" or None,
                "error": "错误信息" or None,
                "usage": {...} or None,
                "retries": 0,
            }
        """
        last_error = None

        for attempt in range(self.retry_config.max_retries + 1):
            try:
                response = self.client.chat.completions.create(
                    model=self.model,
                    messages=messages,
                    temperature=temperature,
                    max_tokens=max_tokens,
                    **kwargs,
                )

                return {
                    "success": True,
                    "content": response.choices[0].message.content,
                    "error": None,
                    "usage": {
                        "prompt_tokens": response.usage.prompt_tokens,
                        "completion_tokens": response.usage.completion_tokens,
                        "total_tokens": response.usage.total_tokens,
                    },
                    "retries": attempt,
                    "finish_reason": response.choices[0].finish_reason,
                }

            except RateLimitError as e:
                last_error = e
                if attempt < self.retry_config.max_retries:
                    delay = self.retry_config.get_delay(attempt)
                    print(f"[限流] 等待 {delay:.1f} 秒后重试...")
                    time.sleep(delay)

            except (APIConnectionError, APITimeoutError, InternalServerError) as e:
                last_error = e
                if attempt < self.retry_config.max_retries:
                    delay = self.retry_config.get_delay(attempt)
                    print(f"[{type(e).__name__}] 等待 {delay:.1f} 秒后重试...")
                    time.sleep(delay)

            except Exception as e:
                # 不可重试的错误
                return {
                    "success": False,
                    "content": None,
                    "error": f"{type(e).__name__}: {str(e)[:300]}",
                    "usage": None,
                    "retries": attempt,
                    "finish_reason": None,
                }

        # 所有重试都失败了
        return {
            "success": False,
            "content": None,
            "error": f"重试 {self.retry_config.max_retries} 次后仍然失败: {type(last_error).__name__}",
            "usage": None,
            "retries": self.retry_config.max_retries,
            "finish_reason": None,
        }

7.3 限流处理

# rate_limiter.py
import time
import threading
from collections import deque
from dataclasses import dataclass


@dataclass
class RateLimitConfig:
    """限流配置"""
    max_requests_per_minute: int = 500
    max_tokens_per_minute: int = 200000
    # 安全余量:实际限制设为官方限制的 80%
    safety_margin: float = 0.8


class TokenBucketRateLimiter:
    """令牌桶限流器"""

    def __init__(self, config: RateLimitConfig = None):
        self.config = config or RateLimitConfig()
        self.lock = threading.Lock()

        # 计算带安全余量的实际限制
        self.max_rpm = int(self.config.max_requests_per_minute * self.config.safety_margin)
        self.max_tpm = int(self.config.max_tokens_per_minute * self.config.safety_margin)

        # 请求记录(滑动窗口)
        self.request_times: deque = deque()
        self.token_usage: deque = deque()  # (timestamp, tokens)

    def _clean_old_records(self):
        """清理超过 1 分钟的旧记录"""
        now = time.time()
        one_minute_ago = now - 60

        while self.request_times and self.request_times[0] < one_minute_ago:
            self.request_times.popleft()

        while self.token_usage and self.token_usage[0][0] < one_minute_ago:
            self.token_usage.popleft()

    def acquire(self, estimated_tokens: int = 0) -> bool:
        """
        尝试获取请求许可

        Args:
            estimated_tokens: 预估的 Token 消耗

        Returns:
            bool: 是否允许发送请求
        """
        with self.lock:
            self._clean_old_records()

            # 检查请求频率
            if len(self.request_times) >= self.max_rpm:
                return False

            # 检查 Token 消耗
            if estimated_tokens > 0:
                current_tokens = sum(t[1] for t in self.token_usage)
                if current_tokens + estimated_tokens > self.max_tpm:
                    return False

            # 记录本次请求
            now = time.time()
            self.request_times.append(now)
            if estimated_tokens > 0:
                self.token_usage.append((now, estimated_tokens))

            return True

    def wait_and_acquire(self, estimated_tokens: int = 0, timeout: float = 30.0) -> bool:
        """等待直到获取许可或超时"""
        start = time.time()

        while time.time() - start < timeout:
            if self.acquire(estimated_tokens):
                return True
            time.sleep(0.1)

        return False

    def get_stats(self) -> dict:
        """获取当前限流统计"""
        with self.lock:
            self._clean_old_records()
            return {
                "requests_this_minute": len(self.request_times),
                "requests_limit": self.max_rpm,
                "requests_usage_pct": round(len(self.request_times) / self.max_rpm * 100, 1),
                "tokens_this_minute": sum(t[1] for t in self.token_usage),
                "tokens_limit": self.max_tpm,
                "tokens_usage_pct": round(
                    sum(t[1] for t in self.token_usage) / self.max_tpm * 100, 1
                ),
            }

8. 多轮对话管理

多轮对话看起来简单,就是把历史消息传回去嘛。但实际做的时候发现有两个棘手的问题:一是上下文越来越长,Token 消耗越来越大;二是超过模型上下文窗口后,前面的对话就丢了。下面是我摸索出来的处理方案。

8.1 对话上下文窗口管理

# conversation_manager.py
from token_tracker import TokenCalculator


class ConversationManager:
    """多轮对话管理器"""

    def __init__(
        self,
        system_prompt: str,
        model: str = "gpt-4o-mini",
        max_context_tokens: int = 8000,
        max_turns: int = 20,
    ):
        self.system_prompt = system_prompt
        self.model = model
        self.max_context_tokens = max_context_tokens
        self.max_turns = max_turns
        self.token_calc = TokenCalculator(model)

        # 对话历史
        self.messages: list[dict] = [
            {"role": "system", "content": system_prompt}
        ]

    def add_user_message(self, content: str):
        """添加用户消息"""
        self.messages.append({"role": "user", "content": content})

    def add_assistant_message(self, content: str):
        """添加助手回复"""
        self.messages.append({"role": "assistant", "content": content})

    def get_messages(self) -> list[dict]:
        """获取当前消息列表(自动裁剪)"""
        self._trim_if_needed()
        return self.messages

    def _trim_if_needed(self):
        """如果上下文过长,自动裁剪"""
        total_tokens = self.token_calc.count_messages_tokens(self.messages)

        if total_tokens <= self.max_context_tokens:
            return

        # 策略:保留 system prompt + 最近的对话
        # 被裁剪的旧对话用摘要替代
        system_msg = self.messages[0]
        conversation = self.messages[1:]

        # 从后往前保留
        kept = []
        current_tokens = self.token_calc.count_tokens(system_msg["content"])

        for msg in reversed(conversation):
            msg_tokens = self.token_calc.count_tokens(msg.get("content", ""))
            if current_tokens + msg_tokens > self.max_context_tokens:
                break
            kept.insert(0, msg)
            current_tokens += msg_tokens

        # 如果有被裁剪的消息,在开头插入摘要提示
        if len(kept) < len(conversation):
            trimmed_count = len(conversation) - len(kept)
            summary_msg = {
                "role": "system",
                "content": f"[注意:由于上下文长度限制,已省略较早的 {trimmed_count} 轮对话。请基于当前可见的对话继续。]",
            }
            self.messages = [system_msg, summary_msg] + kept
        else:
            self.messages = [system_msg] + kept

    def get_conversation_summary(self) -> dict:
        """获取对话摘要"""
        user_messages = [m for m in self.messages if m["role"] == "user"]
        assistant_messages = [m for m in self.messages if m["role"] == "assistant"]

        total_tokens = self.token_calc.count_messages_tokens(self.messages)

        return {
            "total_messages": len(self.messages),
            "user_messages": len(user_messages),
            "assistant_messages": len(assistant_messages),
            "total_turns": len(user_messages),
            "total_tokens": total_tokens,
            "context_usage_pct": round(
                total_tokens / self.max_context_tokens * 100, 1
            ),
        }

    def clear(self):
        """清空对话历史(保留 system prompt)"""
        self.messages = [
            {"role": "system", "content": self.system_prompt}
        ]

8.2 对话摘要与压缩

class ConversationCompressor:
    """对话压缩器 —— 将长对话压缩为摘要"""

    def __init__(self, client: OpenAI, model: str = "gpt-4o-mini"):
        self.client = client
        self.model = model

    def summarize_history(
        self,
        messages: list[dict],
        keep_last_n: int = 3,
    ) -> list[dict]:
        """
        压缩对话历史

        策略:
        1. 保留最近 N 轮对话不变
        2. 将更早的对话压缩为一段摘要
        3. 摘要作为 system 消息的一部分注入
        """
        if len(messages) <= keep_last_n * 2 + 2:
            return messages  # 不需要压缩

        system_msg = messages[0]
        recent = messages[-(keep_last_n * 2):]  # 保留最近 N 轮(每轮 user+assistant)
        old = messages[1:-(keep_last_n * 2)]    # 需要压缩的旧对话

        # 构建摘要请求
        old_conversation = "\n".join([
            f"{'用户' if m['role'] == 'user' else '助手'}: {m.get('content', '')[:200]}"
            for m in old
            if m["role"] in ("user", "assistant")
        ])

        summary_prompt = f"""请将以下对话历史压缩为一段简洁的摘要(不超过200字)。
只保留关键信息:用户的主要问题、助手给出的重要结论、任何需要记住的上下文。

对话历史:
{old_conversation}

摘要:"""

        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": summary_prompt}],
            temperature=0.3,
            max_tokens=300,
        )

        summary = response.choices[0].message.content

        # 构建新的消息列表
        compressed = [
            system_msg,
            {
                "role": "system",
                "content": f"[历史对话摘要] {summary}",
            },
        ] + recent

        return compressed

9. 结构化输出

让 LLM 输出 JSON 格式在实际开发中太常用了——对接前端、存入数据库、调用下游服务,都需要结构化的数据。我摸索了两种方式:

# structured_output.py
import json
from openai import OpenAI


class StructuredOutputClient:
    """结构化输出客户端"""

    def __init__(self, client: OpenAI, model: str = "gpt-4o-mini"):
        self.client = client
        self.model = model

    def json_output(
        self,
        prompt: str,
        system_prompt: str = None,
        temperature: float = 0.1,
    ) -> dict:
        """
        获取 JSON 格式的输出

        使用 response_format 参数强制 JSON 输出
        """
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        messages.append({"role": "user", "content": prompt})

        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=temperature,
            response_format={"type": "json_object"},
        )

        content = response.choices[0].message.content
        return json.loads(content)

    def json_with_schema(
        self,
        prompt: str,
        json_schema: dict,
        system_prompt: str = None,
    ) -> dict:
        """
        获取符合 JSON Schema 的输出

        通过在 Prompt 中嵌入 Schema 来约束输出结构
        """
        schema_text = json.dumps(json_schema, ensure_ascii=False, indent=2)

        full_prompt = f"""{prompt}

【输出格式要求】
请严格按照以下 JSON Schema 输出,确保所有必填字段都存在:

{schema_text}

只输出 JSON,不要包含其他文字。"""

        return self.json_output(full_prompt, system_prompt)


# 使用示例
if __name__ == "__main__":
    client = create_client()
    so_client = StructuredOutputClient(client)

    # 定义输出 Schema
    analysis_schema = {
        "type": "object",
        "properties": {
            "sentiment": {
                "type": "string",
                "enum": ["positive", "negative", "neutral"],
                "description": "情感倾向",
            },
            "confidence": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "置信度",
            },
            "keywords": {
                "type": "array",
                "items": {"type": "string"},
                "description": "关键词列表",
            },
            "summary": {
                "type": "string",
                "maxLength": 100,
                "description": "一句话摘要",
            },
        },
        "required": ["sentiment", "confidence", "keywords", "summary"],
    }

    result = so_client.json_with_schema(
        prompt="分析以下评论:'产品质量很好,但物流太慢了,等了一周才到。'",
        json_schema=analysis_schema,
        system_prompt="你是一个专业的文本分析助手。",
    )
    print(json.dumps(result, ensure_ascii=False, indent=2))

10. 生产环境最佳实践

把代码从"能跑"变成"能上线",中间还有不少坑要填。下面是我整理的一些实践经验。

10.1 架构设计

┌─────────────────────────────────────────────────────────┐
│                   生产环境 API 调用架构                     │
│                                                         │
│  应用层                                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │ 对话管理  │  │ Prompt   │  │ 结果缓存  │              │
│  │          │  │ 模板引擎  │  │          │              │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘              │
│       │             │             │                     │
│       └─────────────┼─────────────┘                     │
│                     │                                   │
│               ┌─────┴─────┐                             │
│               │  API 网关  │  ← 统一入口                  │
│               └─────┬─────┘                             │
│                     │                                   │
│  中间层           ┌──┴──┐                                │
│               │ 限流器 │  ← Token Bucket                │
│               └──┬──┘                                   │
│               ┌──┴──┐                                   │
│               │ 重试  │  ← 指数退避                      │
│               └──┬──┘                                   │
│               ┌──┴──┐                                   │
│               │ 降级  │  ← 主模型不可用时切换备用模型       │
│               └──┬──┘                                   │
│                     │                                   │
│  API 层         ┌───┴───┐                                │
│               │ OpenAI │                                │
│               │  API   │                                │
│               └───────┘                                 │
└─────────────────────────────────────────────────────────┘

10.2 我的实践清单

实践说明优先级
API Key 环境变量化永远不要硬编码 API Key必须
请求日志记录记录每次调用的模型、Token、延迟、成本必须
指数退避重试处理网络波动和限流必须
限流保护避免超出 API 配额必须
超时设置避免请求无限等待必须
内容安全过滤输入和输出都需要安全检查必须
模型降级主模型不可用时切换备用模型推荐
响应缓存相同问题缓存结果,减少 API 调用推荐
异步调用使用异步客户端提升吞吐量推荐
健康检查定期检查 API 可用性推荐

11. 配套项目:AI 对话应用

学完这些之后,我动手搭了一个基于 Streamlit 的 AI 对话应用,把上面讲的所有技术都集成进去了。跑通这个项目,基本就掌握了 OpenAI API 的核心用法。

项目结构

ai-chat-app/
├── app.py                 # Streamlit 主应用
├── config.py              # 配置管理
├── llm_client.py          # OpenAI API 客户端封装
├── function_tools.py      # Function Calling 工具定义与实现
├── prompt_templates.py    # Prompt 模板(结合 Prompt 工程)
├── token_tracker.py       # Token 计算与成本追踪
├── conversation_manager.py # 多轮对话管理
├── requirements.txt       # 项目依赖
└── .env.example           # 环境变量模板

应用功能

  1. 流式对话:AI 回复逐字显示,体验流畅
  2. Function Calling:支持天气查询、数学计算、知识库搜索、时间查询
  3. Token 追踪:实时显示 Token 消耗和成本估算
  4. Prompt 模板切换:内置标准、Few-shot、CoT、ReAct、代码助手、翻译助手六种 Prompt 模式
  5. 错误处理:自动重试、限流保护、优雅降级
  6. 对话导出:支持导出对话记录

项目文件详见配套代码目录 d:\aiagent\学习资料\ai-chat-app\


12. 总结与回顾

我的学习历程

回头看这段学习经历,大概经历了四个阶段:

第一阶段:基础调用
  → 跑通第一个 Chat Completions API 调用
  → 搞懂消息角色体系(system/user/assistant)
  → 摸清 temperature、max_tokens 这些参数的作用

第二阶段:流式与工具
  → 实现流式输出,体验瞬间提升
  → 啃下 Function Calling,让 LLM 能"干活"了
  → 尝试把 Prompt 工程的最佳实践融入 API 调用

第三阶段:工程化
  → 学会计算 Token,开始关注成本
  → 加上错误处理和重试,应用不再动不动就崩
  → 处理多轮对话的上下文管理

第四阶段:打磨
  → 加上限流保护
  → 实现对话压缩
  → 搭了一个完整的 Streamlit 应用来验证

几点心得

  1. API Key 是敏感信息:永远不要提交到版本控制,用 .env 管理
  2. 流式输出是体验的分水岭:加了之后用户感知的等待时间从 5 秒降到几乎为 0
  3. Function Calling 是能力放大器:让 LLM 从"聊天机器人"变成"智能助手"
  4. Token 即成本:每次 API 调用都在花钱,养成计算 Token 的习惯
  5. 错误处理不是可选项:生产环境中网络波动、限流、服务故障是常态,不处理根本没法用
  6. Prompt 工程 + API 工程 = 完整的 AI 应用能力:两者缺一不可

与 Prompt 工程文档的关系

本文是《Prompt 工程指南》的实践篇。我个人是按以下顺序学习的,供参考:

  1. 先读《Prompt 工程指南》,理解 Prompt 设计的理论基础
  2. 再读本文,学习如何通过 API 将 Prompt 设计落地
  3. 跑通配套项目,在实践中巩固所学

我参考的资料


文档版本:v1.0 | 更新时间:2026-05-10 | 适用读者:AI 应用开发者 / LLM 工程师 / 对 OpenAI API 感兴趣的学习者

本文为原创技术文档,配套项目代码位于 https://github.com/yxy008/ai-prompt.git。所有代码示例均可直接运行。如需转载,请注明出处。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐