首页 / 文章 / 无状态聊天循环:在 Python 中手动调用 OpenAI API

无状态聊天循环:在 Python 中手动调用 OpenAI API

通过自行管理消息历史记录,使用 OpenAI Python SDK 构建多轮对话,从而了解为何同样的循环机制是 LangChain 内存系统与智能体的基础。

1167 词

像 LangChain 这样的框架让聊天模型看起来像是具有记忆能力的实体,但实际上底层的 API 并不会记住任何内容。每次调用都是独立的,“对话”实际上只是代码每次重新构建并发送的一系列消息。使用 OpenAI Python SDK 手动编写一次这样的循环,就能清楚地看出智能体框架自动化处理了哪些工作、为什么在对话过程中令牌成本会上升,以及需要提前防范哪些错误。

托管模型端点究竟是什么

OpenAI API 的结构非常简单:服务提供商在自身的 GPU 上运行模型,并通过 HTTPS 提供推理功能。你发送文本后,模型会按照常规的下一令牌生成流程产生令牌,而你需要为双向传输的每个令牌付费。由此会产生三个后果:

  1. 它是无状态的。之前的请求内容不会被保留,因此每个请求都必须包含模型所需的所有信息。
  • 这是普通的 HTTP 请求。SDK 会封装 POST 请求,因此当出现故障时你可以查看原始流量。
  • 你购买的是令牌而非答案。任何冗长的提示语都会在每次包含它的调用中产生费用。
  • 绝不要将 API 密钥放入源代码中。密钥可能通过 Git 历史记录、截图以及共享笔记本泄露,一旦密钥被窃,就意味着会有其他人使用你的账户消费。SDK 会自动从环境变量中读取 OPENAI_API_KEY,因此你的脚本完全无需编写任何处理密钥的代码。API 使用费用与 ChatGPT 订阅费是分开计费的,新账户通常需要一定的预付费余额。

    角色:共享消息格式

    一个请求会携带一系列消息,每条消息都有一个角色:

    • system 用于存储你的指令,模型会给予这些指令更高的权重。
    • user 用于存储用户输入的内容。
  • assistant用于存储模型之前的回复,在智能体中则用于保存其调用的工具信息。
  • 这种格式在整个生态系统中都是通用的。Claude和Gemini采用了类似的理念但存在细微差别,Ollama也模仿了这一结构,而LangChain的SystemMessage、HumanMessage和AIMessage则是以类形式体现的这些角色。在生成内容之前,提供方会将列表整合为一个token序列,因此这些角色实际上构成了结构化的提示工程。

    单个请求

    第一个示例创建了一个客户端,该客户端从环境变量中读取密钥,发送系统指令和一个问题,然后打印出回复以及来自usage的提示词和完成token数量:

    from openai import OpenAI
    
    client = OpenAI()  # key from env
    
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a concise "
                           "Python assistant.",
            },
            {
                "role": "user",
                "content": "Why resend the whole "
                           "chat history each call?",
            },
        ],
        temperature=0,
    )
    print(resp.choices[0].message.content)
    u = resp.usage
    print(u.prompt_tokens, u.completion_tokens)
    

    gpt-4o-mini 是一款适合学习的低成本模型;要换成更大型的模型只需修改一行代码,不过模型名称和价格会变化,因此请查看最新列表。temperature=0 可最大程度降低采样随机性,这是问答场景以及后续工具调用场景的理想默认值。每次调用时都要记录usage数据,它就是你的成本计量器。

    自行进行对话

    由于服务器会忘记所有内容,历史记录由你的代码负责管理:每次调用后,代码都会存储回复、添加下一个问题,然后再将所有内容重新提交。下面的辅助函数通过模块级的msgs列表来实现这一功能,该列表以一条系统消息作为开头:

    msgs = [{
        "role": "system",
        "content": "You are a concise assistant.",
    }]
    
    def ask(text: str) -> str:
        msgs.append(
            {"role": "user", "content": text}
        )
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=msgs,  # full history
            temperature=0,
        )
        reply = resp.choices[0].message.content
        msgs.append({
            "role": "assistant",
            "content": reply,
        })
        return reply
    
    print(ask("Define a context window."))
    print(ask("Now for a five-year-old."))
    print(ask("Which answer was shorter?"))
    

    第三个问题正好证明了这一点。模型只能比较这两个答案,因为它们都在msgs中并被重新发送。如果去掉添加助手回复的那行代码,模型就完全无法理解你的意图。

    这个ask()函数会以多种形式出现。ChatGPT网页应用本质上就是带有用户界面的该函数实现。LangChain的RunnableWithMessageHistory则是对添加内容并重新发送这一过程的封装版本。智能体的内部循环也采用相同模式,只是多了工具调用和结果处理步骤。需要注意的是,三次重新发送的操作实际上等价于一次或两次,因此每次交互后输入的标记数量都会增加。

    运行示例

    先安装所需依赖,然后在shell中导出密钥,最后运行脚本。此处显示的密钥仅为占位符,请通过shell或密钥管理工具提供自己的密钥,绝不能将其保存在已提交的文件中:

    pip install -r requirements.txt
    export OPENAI_API_KEY="sk-..."
    python examples/part02_chat.py
    

    该脚本首先进行单轮调用,然后执行三轮对话;每次请求后都会报告令牌使用情况以及大致费用。如果缺少密钥,它会立即停止运行,并且故意不将其纳入持续集成流程中,因为该功能需要实际支付费用并使用真实密钥。如果想要为这类代码编写测试,可以对客户端进行模拟。

    需提前考虑的隐患

    1. 密钥缺失:会出现HTTP 401状态的AuthenticationError错误,通常是因为该变量在此环境中未被设置、拼写有误或包含粘贴带来的空白字符。应在程序启动时进行检查并立即报错。
    2. 速率限制:出现HTTP 429状态的RateLimitError错误,表示请求次数过多或预付费余额已耗尽。循环运行的代理程序容易遇到此问题,因此应立即添加带延迟的重试机制。
  • 历史记录在两种情况下都会出问题:如果忘记追加,模型就会忘掉所有内容;如果一直追加,令牌消耗会呈二次方增长,最终导致上下文长度错误。实际系统会裁剪、总结或仅检索相关的历史记录,而这一理念正是RAG的核心。
  • 温度值为0时输出虽然稳定,但并不准确。对于任何重要的内容都需进行验证。
  • 不同服务提供商的格式一致

    Claude 接收用户和助手的消息列表,系统提示被移到了单独的顶层参数中。Gemini 采用相同的对话列表模型,角色分别命名为 user 和 model。Ollama 提供了兼容 OpenAI 的接口,因此通过修改基础 URL 和模型名称,该代码即可调用本地模型;详情请参见 通过兼容 OpenAI 的接口调用 Claude、GPT 和 Gemini。正是这种一致性使得 LangChain 能够为众多服务提供商提供统一的抽象接口。

    关键要点

    • 聊天 API 是无状态的;代码负责管理并重新发送对话内容。
    • 带有角色标签的消息列表实际上已成为跨服务提供商的标准格式。
    • 由于每轮对话的输入token量都会增加,需记录每次调用的 usage 数据。
  • 将密钥保留在环境中,避免在 CI 流程中发起实时 API 调用。
  • 在构建代理之前先处理 401、429 错误以及无限制的历史记录问题,因为代理循环会加剧这三种问题的出现。