第 6 章:AI Agent 基础

本章将带你从零开始理解 AI Agent 的核心基础。就像前端开发需要理解 DOM、事件循环一样,掌握 LLM 原理和 API 调用是构建 AI 应用的起点。我们将用前端开发者熟悉的类比,帮你快速建立直觉。

6.1 LLM 原理速览

大语言模型(Large Language Model,LLM)是驱动当前 AI 浪潮的核心引擎。作为前端开发者,你可以把它想象成一个超级强大的"文本生成函数"——你传入一段文本(Prompt),它返回一段连贯的、符合上下文的文本。

什么是大语言模型

LLM 本质上是一个经过海量文本训练的神经网络。它的核心能力可以概括为:给定一段文本,预测下一个最可能出现的词(Token)。这个过程不断重复,就能生成一整段文章、代码甚至对话。

ℹ️前端类比

把 LLM 想象成浏览器的自动补全功能,但它不是基于简单的词典匹配,而是基于对整个互联网文本的"理解"。当你输入"React hooks 的",它可能补全出"useState 用于管理组件状态",因为它"见过"无数类似的教程。

Transformer 架构与注意力机制

当前主流 LLM(GPT、Claude、通义千问等)都基于 Transformer 架构。其核心创新是自注意力机制(Self-Attention),让模型在处理每个词时,都能"看到"并权衡句子中其他所有词的重要性。

注意力机制的工作方式类似你在阅读长文时的行为:读到"它"时,你的眼睛会快速回扫,找到"它"指代的对象。Transformer 通过数学方式(Query-Key-Value 计算)实现了这种"全局关联"能力。

python
# 注意力机制的直观理解(简化版)
# 想象你在翻译 "The cat sat on the mat, it was warm."
# 处理 "it" 时,注意力权重会集中在 "mat" 上

sentence = ["The", "cat", "sat", "on", "the", "mat", "it", "was", "warm"]

# 处理 "it" 时,各词的注意力权重(示意)
attention_weights_for_it = {
    "The": 0.01,
    "cat": 0.05,
    "sat": 0.02,
    "on": 0.02,
    "the": 0.05,
    "mat": 0.75,   # "it" 最可能指代 "mat"
    "it": 0.00,
    "was": 0.05,
    "warm": 0.05
}

Token 化

LLM 处理的不是原始字符,而是 Token。Token 是文本的最小处理单元,可能是完整单词、单词的一部分,甚至单个汉字。

⚠️注意

中文的 Token 化比较特殊:一个汉字通常占 1-2 个 Token,而英文单词可能只占 1 个 Token。这意味着同样的信息量,中文 Prompt 消耗的 Token 通常更多,成本也更高。

python
import tiktoken

# 获取 GPT-4 的编码器
encoder = tiktoken.encoding_for_model("gpt-4")

text = "Hello world! 你好世界!"
tokens = encoder.encode(text)

print(f"文本: {text}")
print(f"Token 数量: {len(tokens)}")
print(f"Token ID 列表: {tokens}")

# 解码查看每个 Token
for i, token_id in enumerate(tokens):
    token_text = encoder.decode([token_id])
    print(f"  Token {i}: ID={token_id}, 文本='{token_text}'")

# 输出示例:
# Token 数量: 10
# Token ID 列表: [9906, 1917, 0, 57668, 53901, 12131, 10401, 245, 11319, 63823]

上下文窗口

上下文窗口(Context Window)是 LLM 能同时"记住"的最大 Token 数量。就像浏览器的 localStorage 有容量限制一样,LLM 的"记忆"也有上限。

模型上下文窗口说明
GPT-3.516K tokens约 12K 汉字
GPT-4128K tokens约 96K 汉字
Claude 3.5200K tokens可处理整本书
DeepSeek-V364K tokens国产高性价比
ℹ️前端类比

上下文窗口就像 React 的 state——它决定了组件能"记住"多少数据。超出窗口的旧消息会被"遗忘",这就是为什么长对话中 LLM 可能会"忘记"你之前说过的话。

6.2 API 调用基础

与 LLM 交互的主要方式是通过 HTTP API。作为前端开发者,你对 fetch/axios 已经很熟悉,调用 LLM API 的原理完全相同——只是请求体和响应体的结构有特定规范。

OpenAI API 格式

OpenAI 的 API 格式已成为事实标准,大多数国产模型也兼容此格式。核心端点是 /v1/chat/completions

python
import requests

# OpenAI API 调用示例(原生 HTTP)
API_KEY = "sk-your-api-key"

response = requests.post(
    "https://api.openai.com/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json"
    },
    json={
        "model": "gpt-4o-mini",
        "messages": [
            {"role": "system", "content": "你是一个 helpful 的助手"},
            {"role": "user", "content": "用 Python 写一个快速排序"}
        ],
        "temperature": 0.7,  # 创造性程度:0=保守,2=奔放
        "max_tokens": 1000   # 最大输出 Token 数
    }
)

data = response.json()
print(data["choices"][0]["message"]["content"])

国产大模型 API 对比

国内开发者常用的模型及接入方式:

厂商模型API 格式特点
阿里云通义千问 (qwen)OpenAI 兼容中文能力强,文档丰富
百度文心一言 (ernie)自有格式企业场景优化
DeepSeekDeepSeek-V3OpenAI 兼容性价比极高,推理强
月之暗面KimiOpenAI 兼容长上下文(200K)
python
# DeepSeek API 调用(兼容 OpenAI 格式)
import requests

response = requests.post(
    "https://api.deepseek.com/v1/chat/completions",
    headers={
        "Authorization": "Bearer sk-your-deepseek-key",
        "Content-Type": "application/json"
    },
    json={
        "model": "deepseek-chat",
        "messages": [
            {"role": "user", "content": "解释什么是闭包"}
        ],
        "stream": False  # 非流式输出
    }
)

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

6.3 使用 Python 调用 LLM

虽然用 requests 可以直接调用 API,但官方 SDK 提供了更完善的错误处理、重试机制和流式输出支持。

安装 openai 库

bash
pip install openai python-dotenv

基础调用与错误处理

python
import os
from openai import OpenAI, APIError, RateLimitError, APIConnectionError
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

# 初始化客户端(可指向任意兼容 OpenAI 的 API)
client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
)

def chat_with_llm(messages, model="gpt-4o-mini", temperature=0.7):
    """
    与 LLM 对话,包含完整的错误处理
    """
    try:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            temperature=temperature,
            max_tokens=2000
        )
        return {
            "success": True,
            "content": response.choices[0].message.content,
            "usage": {
                "prompt_tokens": response.usage.prompt_tokens,
                "completion_tokens": response.usage.completion_tokens,
                "total_tokens": response.usage.total_tokens
            }
        }
    except RateLimitError:
        return {"success": False, "error": "请求过于频繁,请稍后再试"}
    except APIConnectionError:
        return {"success": False, "error": "网络连接失败,请检查网络"}
    except APIError as e:
        return {"success": False, "error": f"API 错误: {e.message}"}
    except Exception as e:
        return {"success": False, "error": f"未知错误: {str(e)}"}

# 使用示例
result = chat_with_llm([
    {"role": "user", "content": "用一句话解释 REST API"}
])

if result["success"]:
    print(f"回复: {result['content']}")
    print(f"消耗 Token: {result['usage']['total_tokens']}")
else:
    print(f"错误: {result['error']}")

流式输出

流式输出(Streaming)让 LLM 可以"逐字逐句"地返回内容,而不是等全部生成完才返回。这能显著提升用户体验,就像你看到的 ChatGPT 打字效果。

python
def stream_chat(messages, model="gpt-4o-mini"):
    """
    流式对话,实时输出每个 Token
    """
    try:
        stream = client.chat.completions.create(
            model=model,
            messages=messages,
            stream=True,  # 启用流式输出
            max_tokens=2000
        )

        full_content = ""
        for chunk in stream:
            # 每个 chunk 包含一个增量 Token
            delta = chunk.choices[0].delta.content
            if delta:
                full_content += delta
                print(delta, end="", flush=True)  # 实时打印

        print()  # 换行
        return full_content

    except Exception as e:
        print(f"\n错误: {e}")
        return None

# 使用示例:你会看到文字像打字一样逐字出现
stream_chat([
    {"role": "user", "content": "讲一个关于程序员的短笑话"}
])
ℹ️前端类比

非流式调用就像同步的 fetch(),等全部数据返回才渲染;流式调用就像 WebSocket 或 Server-Sent Events,数据一点一点到达,可以边收边渲染。

6.4 Prompt Engineering

Prompt Engineering(提示工程)是设计和优化输入文本,让 LLM 输出更精准结果的技术。作为前端开发者,你可以把它理解为"给函数传正确的参数"。

角色设定(System Prompt)

system 消息用于设定 AI 的角色和行为准则。它就像 CSS 的 :root 变量,定义了整个对话的"全局样式"。

python
# 好的 System Prompt 应该具体、明确
messages = [
    {
        "role": "system",
        "content": """你是一位资深前端工程师,正在指导后端开发者学习 React。
        你的回答特点:
        1. 用后端开发者熟悉的概念做类比
        2. 每个概念都附带可运行的代码示例
        3. 解释"为什么"而不仅仅是"怎么做"
        4. 语气友好但专业"""
    },
    {"role": "user", "content": "什么是 React 的 useEffect?"}
]

# 差的 System Prompt:过于笼统
# "你是一个 helpful 的助手"  -- 没有提供任何行为指导

Few-shot 示例

Few-shot 是指在 Prompt 中提供几个输入-输出示例,让模型"模仿"这种模式。就像你给组件写几个 Storybook 示例,其他开发者一看就知道怎么用。

python
# Few-shot:让模型按固定格式提取信息
messages = [
    {"role": "system", "content": "从用户输入中提取姓名、年龄和城市,按 JSON 格式输出。"},
    {"role": "user", "content": "我叫张三,今年 25 岁,住在上海。"},
    {"role": "assistant", "content": '{"name": "张三", "age": 25, "city": "上海"}'},
    {"role": "user", "content": "李四,30 岁,北京人。"},
    {"role": "assistant", "content": '{"name": "李四", "age": 30, "city": "北京"}'},
    # 真正的输入
    {"role": "user", "content": "王五今年 28,来自深圳。"}
]

Chain-of-Thought 思维链

对于复杂问题,让模型"一步一步思考"(step by step)能显著提升准确率。这就像你在调试 Bug 时,不会直接猜原因,而是逐步排查。

python
# 不加思维链:模型可能直接猜答案,容易出错
# 用户:15 个工人 8 天修路 2400 米,30 个工人 12 天能修多少米?
# 模型可能直接输出一个错误数字

# 加思维链:引导模型逐步计算
messages = [
    {"role": "user", "content": """15 个工人 8 天修路 2400 米,30 个工人 12 天能修多少米?

请按以下步骤思考:
1. 先计算每个工人每天修多少米
2. 再计算 30 个工人 12 天能修多少米
3. 给出最终答案"""}
]

# 模型会输出类似:
# 1. 每个工人每天修:2400 / 15 / 8 = 20 米
# 2. 30 个工人 12 天修:20 * 30 * 12 = 7200 米
# 3. 最终答案:7200 米

输出格式控制(JSON)

让模型输出结构化 JSON 是构建应用的关键技能。现代模型(GPT-4、Claude 3.5 等)支持 JSON Mode,可以强制输出合法 JSON。

python
import json

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "你是一个分类器,将用户评论分类为正面/负面/中性。"},
        {"role": "user", "content": "这个手机电池太差了,一天要充三次电!"}
    ],
    response_format={"type": "json_object"},  # 强制 JSON 输出
    max_tokens=500
)

# 解析 JSON 结果
result = json.loads(response.choices[0].message.content)
print(result)
# 输出: {"sentiment": "负面", "confidence": 0.95, "keywords": ["电池差", "充电频繁"]}
💡技巧

使用 JSON Mode 时,务必在 system 或 user 消息中明确说明期望的 JSON 结构,否则模型可能输出不符合预期的字段。

6.5 Function Calling / Tools

Function Calling(函数调用)是 LLM 最强大的能力之一。它让模型不仅能"说话",还能"行动"——根据上下文判断是否需要调用外部工具(如查天气、算数学、查数据库)。

ℹ️前端类比

Function Calling 就像 React 的事件系统:用户触发事件(发送消息),系统判断调用哪个处理器(选择函数),执行后更新状态(返回结果给模型)。

定义工具函数

python
# 定义可供模型调用的工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如'北京'、'上海'"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "执行数学计算",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式,如'2 + 3 * 4'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

让模型选择并调用

python
import json

# 模拟工具函数实现
def get_weather(city):
    """模拟天气查询"""
    weather_db = {
        "北京": "晴天,25°C",
        "上海": "多云,28°C",
        "深圳": "小雨,30°C"
    }
    return weather_db.get(city, "未知城市")

def calculate(expression):
    """安全计算数学表达式"""
    try:
        # 使用 eval 前做安全检查(仅允许数字和运算符)
        allowed_chars = set("0123456789+-*/.() ")
        if not all(c in allowed_chars for c in expression):
            return "表达式包含非法字符"
        return str(eval(expression))
    except Exception as e:
        return f"计算错误: {e}"

# 工具映射表
tool_map = {
    "get_weather": get_weather,
    "calculate": calculate
}

def chat_with_tools(user_message):
    """
    支持 Function Calling 的对话
    """
    messages = [
        {"role": "system", "content": "你是一个 helpful 助手,可以使用工具帮助用户。"},
        {"role": "user", "content": user_message}
    ]

    # 第一次调用:让模型决定是否需要调用工具
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        tools=tools,
        tool_choice="auto"  # 让模型自动决定
    )

    message = response.choices[0].message

    # 检查模型是否要求调用工具
    if message.tool_calls:
        # 将模型的工具调用请求加入对话历史
        # 将模型的工具调用请求加入对话历史(使用 model_dump 兼容新版 SDK)
        messages.append(message.model_dump(exclude_none=True))

        # 执行每个工具调用
        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)

            print(f"模型决定调用: {func_name}({func_args})")

            # 执行函数
            result = tool_map[func_name](**func_args)

            # 将结果返回给模型
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(result)
            })

        # 第二次调用:让模型基于工具结果生成回复
        final_response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages
        )
        return final_response.choices[0].message.content

    else:
        # 模型直接回复,不需要调用工具
        return message.content

# 测试:模型会自动识别需要调用哪个工具
print(chat_with_tools("北京今天天气怎么样?"))
print(chat_with_tools("123 乘以 456 等于多少?"))
💡技巧

工具函数的 description 非常重要——模型就是根据描述来决定是否调用该工具的。描述要清晰、具体,说明什么场景下应该使用。

6.6 多轮对话管理

LLM 本身是无状态的,每次 API 调用都是独立的。要实现"多轮对话",需要由应用层维护对话历史,并在每次请求时把完整历史发送给模型。

维护对话历史

python
class ConversationManager:
    """
    多轮对话管理器
    """
    def __init__(self, system_prompt="你是一个 helpful 助手", max_history=10):
        self.messages = [
            {"role": "system", "content": system_prompt}
        ]
        self.max_history = max_history  # 保留最近 N 轮对话

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

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

    def get_messages(self):
        """获取当前对话历史(包含上下文截断)"""
        # 保留 system 消息 + 最近 max_history 轮对话
        if len(self.messages) <= self.max_history * 2 + 1:
            return self.messages

        # system 消息 + 最近的对话
        return [self.messages[0]] + self.messages[-(self.max_history * 2):]

    def clear(self):
        """清空对话(保留 system)"""
        system = self.messages[0]
        self.messages = [system]

    def estimate_tokens(self):
        """
        估算当前对话的 Token 数量(粗略估算)
        """
        total_chars = sum(len(m["content"]) for m in self.messages)
        # 中文字符约 1.5 Token/字,英文约 0.25 Token/字符
        return int(total_chars * 0.6)

# 使用示例
conv = ConversationManager("你是一个 Python 导师", max_history=5)

conv.add_user_message("什么是列表推导式?")
# -> 调用 API,获取回复
conv.add_assistant_message("列表推导式是 Python 中一种简洁的创建列表的方式...")

conv.add_user_message("和 map 有什么区别?")
# -> 调用 API 时,模型能看到前面的对话,理解"它"指代"列表推导式"
print(conv.get_messages())

上下文截断策略

当对话历史超过模型上下文窗口时,需要智能地截断。常见策略:

  1. 滑动窗口(Sliding Window)

    只保留最近 N 轮对话。实现简单,但会丢失早期上下文。适合大多数聊天场景。

  2. 摘要压缩(Summarization)

    当对话过长时,让模型生成一段摘要,替换掉早期的详细对话。保留关键信息的同时节省 Token。

  3. Token 预算管理

    为 system 消息、对话历史、输出分别预留 Token 配额,确保总长度不超过上下文窗口。

python
def summarize_history(messages, client, model="gpt-4o-mini"):
    """
    将早期对话压缩为摘要
    """
    summary_prompt = """请将以下对话历史总结为一段简洁的摘要,保留所有关键信息和决策。
    摘要应该让没有看过原始对话的人也能理解上下文。"""

    history_text = "\n".join([
        f"{'用户' if m['role'] == 'user' else '助手'}: {m['content'][:200]}"
        for m in messages[1:-4]  # 排除 system 和最近 2 轮
    ])

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

    summary = response.choices[0].message.content
    return [
        messages[0],  # system
        {"role": "user", "content": f"【历史摘要】{summary}"},
        *messages[-4:]  # 最近 2 轮完整对话
    ]

成本估算

LLM API 按 Token 数量计费。了解成本结构有助于优化应用:

模型输入(每 1M tokens)输出(每 1M tokens)
GPT-4o-mini$0.15$0.60
GPT-4o$2.50$10.00
Claude 3.5 Sonnet$3.00$15.00
DeepSeek-V3约 $0.14约 $0.28
⚠️注意

输入 Token(Prompt + 历史对话)和输出 Token(模型回复)分别计费。长对话中,历史消息的 Token 消耗往往超过新回复,定期清理或摘要能显著降低成本。

6.7 实战:命令行 AI 助手

综合运用本章所学,构建一个支持多轮对话、流式输出和优雅退出的命令行 AI 助手。

python
#!/usr/bin/env python3
"""
cli_ai_assistant.py
命令行 AI 助手 - 支持多轮对话和流式输出
"""

import os
import sys
from openai import OpenAI
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

class CLIAssistant:
    def __init__(self):
        self.client = OpenAI(
            api_key=os.getenv("OPENAI_API_KEY"),
            base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
        )
        self.model = os.getenv("LLM_MODEL", "gpt-4o-mini")
        self.messages = [
            {
                "role": "system",
                "content": "你是一个 helpful 的编程助手。回答简洁,代码示例要完整可运行。"
            }
        ]
        self.total_tokens = 0

    def stream_chat(self, user_input):
        """流式对话"""
        self.messages.append({"role": "user", "content": user_input})

        try:
            stream = self.client.chat.completions.create(
                model=self.model,
                messages=self.messages,
                stream=True,
                max_tokens=2000
            )

            print("助手: ", end="", flush=True)
            full_response = ""

            for chunk in stream:
                delta = chunk.choices[0].delta.content
                if delta:
                    print(delta, end="", flush=True)
                    full_response += delta

            print()  # 换行
            self.messages.append({"role": "assistant", "content": full_response})

            # 简单的 Token 估算
            self.total_tokens += len(user_input) + len(full_response)

        except Exception as e:
            print(f"\n错误: {e}")

    def show_stats(self):
        """显示对话统计"""
        print(f"\n--- 统计 ---")
        print(f"对话轮数: {(len(self.messages) - 1) // 2}")
        print(f"估算 Token: {self.total_tokens}")
        print(f"------------\n")

    def clear_history(self):
        """清空对话历史"""
        system = self.messages[0]
        self.messages = [system]
        self.total_tokens = 0
        print("对话历史已清空。\n")

    def run(self):
        """主循环"""
        print("=" * 50)
        print("  命令行 AI 助手")
        print("  输入 /quit 退出,/clear 清空历史,/stats 查看统计")
        print("=" * 50)
        print()

        while True:
            try:
                user_input = input("你: ").strip()
            except (KeyboardInterrupt, EOFError):
                print("\n再见!")
                break

            if not user_input:
                continue

            # 处理命令
            if user_input == "/quit":
                print("再见!")
                break
            elif user_input == "/clear":
                self.clear_history()
                continue
            elif user_input == "/stats":
                self.show_stats()
                continue

            self.stream_chat(user_input)
            print()

if __name__ == "__main__":
    assistant = CLIAssistant()
    assistant.run()

运行方式

bash
# 1. 创建 .env 文件
cat > .env << 'EOF'
OPENAI_API_KEY=sk-your-api-key
OPENAI_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o-mini
EOF

# 2. 安装依赖
pip install openai python-dotenv

# 3. 运行
python cli_ai_assistant.py
💡扩展思路

在这个基础上,你可以添加:命令历史(用 readline 模块)、彩色输出(colorama)、本地配置文件、多模型切换、导出对话记录等功能。

6.8 多模态 API(图片理解)

现代 LLM(GPT-4o、Claude 3.5、通义千问 VL 等)不仅理解文本,还能"看懂"图片。 你可以上传一张截图,让模型解释界面内容;上传一张图表,让模型提取数据;上传设计稿,让模型生成代码。

python
import base64
from openai import OpenAI

client = OpenAI()

def encode_image(image_path: str) -> str:
    """将本地图片编码为 base64"""
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")

def analyze_image(image_path: str, question: str) -> str:
    """分析图片内容"""
    base64_image = encode_image(image_path)

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": question},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/png;base64,{base64_image}",
                        "detail": "auto"  # auto / low / high
                    }
                }
            ]
        }],
        max_tokens=1000
    )

    return response.choices[0].message.content

# 使用示例
# result = analyze_image("screenshot.png", "这个界面有哪些设计问题?")
# result = analyze_image("chart.png", "从这张图表中提取数据")
# result = analyze_image("error.png", "这个错误是什么意思?如何修复?")
💡多模态应用场景
  • UI 审查:上传截图,让 AI 分析设计和可访问性
  • 图表分析:从数据图表中提取数值,生成报表
  • OCR + 理解:识别图片中的文字并进行语义分析
  • 代码生成:上传设计稿(Figma 导出),生成前端代码

6.9 Token 计费与成本优化

LLM API 按 Token 数量计费。理解 Token 计数和优化策略,可以帮助你控制成本, 避免收到意外的账单。这就像是管理云服务器的资源使用一样重要。

精确计算 Token 数量

python
"""Token 计数与成本估算 (pip install tiktoken)"""
import tiktoken

# 获取编码器
encoder = tiktoken.encoding_for_model("gpt-4o")

def count_tokens(text: str, model: str = "gpt-4o") -> int:
    """计算文本的 Token 数量"""
    encoder = tiktoken.encoding_for_model(model)
    return len(encoder.encode(text))

def count_messages_tokens(messages: list[dict], model: str = "gpt-4o") -> int:
    """计算完整消息列表的 Token 数量

    这包括消息内容 + 每条消息的固定开销(约 3-4 tokens/message)
    """
    encoder = tiktoken.encoding_for_model(model)
    total = 0
    for msg in messages:
        total += 3  # 每条消息的基本开销
        for key, value in msg.items():
            total += len(encoder.encode(str(value)))
        if "name" in msg:
            total += 1  # name 字段的额外开销
    total += 3  # 每次请求的开销
    return total

def estimate_cost(prompt_tokens: int, completion_tokens: int, model: str = "gpt-4o-mini") -> dict:
    """估算 API 调用成本"""
    pricing = {
        "gpt-4o": (0.0025, 0.01),      # (输入, 输出) 每 1K tokens 的美元价格
        "gpt-4o-mini": (0.00015, 0.0006),
        "gpt-4": (0.03, 0.06),
    }

    input_price, output_price = pricing.get(model, (0, 0))

    cost = (prompt_tokens * input_price + completion_tokens * output_price) / 1000

    return {
        "model": model,
        "prompt_tokens": prompt_tokens,
        "completion_tokens": completion_tokens,
        "estimated_cost_usd": round(cost, 6),
        "estimated_cost_cny": round(cost * 7.2, 4),  # 换算人民币
    }

# 使用示例
messages = [
    {"role": "system", "content": "你是一个 Python 助手"},
    {"role": "user", "content": "解释装饰器的用法"}
]

tokens = count_messages_tokens(messages, "gpt-4o-mini")
print(f"输入 Token: {tokens}")
print(estimate_cost(tokens, 500, "gpt-4o-mini"))

成本优化策略

⚠️省钱清单
  1. 选择便宜的模型:GPT-4o-mini 比 GPT-4o 便宜约 16 倍,大多数场景够用
  2. 缩短 Prompt:system prompt 不要超过 200 字,用清晰的指令而不是长篇大论
  3. 缓存 System Prompt:OpenAI 和 Anthropic 对重复使用的 system prompt 自动缓存,费用减半
  4. 限制历史轮数:保留最近 5-10 轮对话即可,超出部分用摘要替代
  5. 设置 max_tokens:合理限制输出长度,避免模型"滔滔不绝"浪费 Token
  6. 使用本地模型:量大的固定任务(如分类、翻译)可考虑使用 Ollama + 开源模型

6.10 JSON Schema 结构化输出

除了简单的 JSON Mode,新版本 API(GPT-4o、GPT-4 等)支持通过 JSON Schema 严格约束输出结构。这比手写自然语言描述更可靠:

python
# 使用 JSON Schema 强制结构化输出(OpenAI SDK v1.40+)
# 这比 JSON Mode + 自然语言描述更可靠

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": "分析这段代码的问题:for i in range(len(items)): print(items[i])"
    }],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "code_review",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "issues": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "severity": {
                                    "type": "string",
                                    "enum": ["error", "warning", "info"]
                                },
                                "line": {"type": "integer"},
                                "description": {"type": "string"},
                                "suggestion": {"type": "string"}
                            },
                            "required": ["severity", "description", "suggestion"],
                            "additionalProperties": False
                        }
                    },
                    "summary": {"type": "string"}
                },
                "required": ["issues", "summary"],
                "additionalProperties": False
            }
        }
    }
)

result = json.loads(response.choices[0].message.content)
print(json.dumps(result, indent=2, ensure_ascii=False))
💡使用建议

JSON Schema 的输出非常可靠,适合生产环境。但注意:strict=True 会增加少许延迟, 对于实时交互场景(如聊天机器人),可以关闭 strict 模式。