关于GPT应用的几点记录

架构图画得再漂亮,一进项目还是会被 Token 账单和上下文窗口教做人。

第一次碰 OpenAI API 是 2023 年初。当时想法很天真:前端加个输入框,后端调 API,把答案丢回去,就算"完成 AI 改造"。

最早版本:一个对话脚本

那时候的代码大概长这样:

from openai import OpenAI

client = OpenAI(api_key="sk-xxx")

def chat(message):
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": "你是一个有用的助手。"},
            {"role": "user", "content": message}
        ]
    )
    return response.choices[0].message.content

print(chat("帮我写一个 Python 函数反转列表"))

这个版本能对话,但问题很多:

  1. 没记忆,每次对话都是独立的
  2. 没结构化输出,想提取内容还得自己解析
  3. 没错误处理,网络异常、超时、限流都是直接挂
  4. 没成本控制,用户聊多了 Token 费用不可控

第一个踩的坑是 Token 计费。有个测试用户连续问了十几个复杂问题,测试账号几分钟就烧了 2 美元。后来加上 Token 计数和限制:

import tiktoken

def count_tokens(text, model="gpt-3.5-turbo"):
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))

def chat_with_limit(message, max_tokens=1000):
    input_tokens = count_tokens(message)
    if input_tokens > max_tokens:
        return "问题太长,请简化后重试。"

    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "system", "content": "你是一个有用的助手。"},
                  {"role": "user", "content": message}],
        max_tokens=max_tokens - input_tokens
    )
    return response.choices[0].message.content

加上记忆:从对话到上下文

要让它"记住"之前的对话,最简单的做法是把历史消息都塞进去:

class ChatSession:
    def __init__(self):
        self.messages = [
            {"role": "system", "content": "你是一个有用的助手。"}
        ]

    def chat(self, message):
        self.messages.append({"role": "user", "content": message})
        response = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=self.messages
        )
        answer = response.choices[0].message.content
        self.messages.append({"role": "assistant", "content": answer})
        return answer

这个版本跑了一段时间,问题又来了:对话一长,Token 就爆了,而且无关的历史对话会干扰当前问题。

后来改用滑动窗口,只保留最近 N 条:

from collections import deque

class ChatSessionWithWindow:
    def __init__(self, window_size=6):
        self.messages = deque(maxlen=window_size)
        self.messages.append(
            {"role": "system", "content": "你是一个有用的助手。"}
        )

    def chat(self, message):
        self.messages.append({"role": "user", "content": message})
        response = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=list(self.messages)
        )
        answer = response.choices[0].message.content
        self.messages.append({"role": "assistant", "content": answer})
        return answer

但这样还是不够。真正的问题是:什么内容需要记住?什么可以丢掉?如果用户在第三轮说了"我需要 Python 相关的回答",第十轮又问"怎么反转数组",这时候系统提示早就被挤出窗口了。

后来试了几种方案:

  1. 分层记忆:系统提示+长期记忆+短期对话
  2. 关键信息提取:每轮对话后自动提取关键信息
  3. 用户显式控制:允许用户标记"记住这个"或"忘掉刚才的"

最后在项目里实际用的是简化版分层:

class MemoryAssistant:
    def __init__(self):
        self.system_prompt = "你是一个专业的编程助手。"
        self.long_term_memory = {}  # key: user_id, value: summary
        self.conversation_history = {}  # key: session_id, value: deque

    def get_system_messages(self, user_id):
        messages = [{"role": "system", "content": self.system_prompt}]
        if user_id in self.long_term_memory:
            messages.append({
                "role": "system",
                "content": f"用户背景:{self.long_term_memory[user_id]}"
            })
        return messages

    def chat(self, user_id, session_id, message):
        # 获取或初始化会话历史
        if session_id not in self.conversation_history:
            self.conversation_history[session_id] = deque(maxlen=6)

        # 构建完整消息
        messages = self.get_system_messages(user_id)
        messages.extend(list(self.conversation_history[session_id]))
        messages.append({"role": "user", "content": message})

        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages
        )
        answer = response.choices[0].message.content

        # 更新对话历史
        self.conversation_history[session_id].append(
            {"role": "user", "content": message}
        )
        self.conversation_history[session_id].append(
            {"role": "assistant", "content": answer}
        )

        return answer

结构化输出:从文本到 JSON

初期为了让 AI 返回结构化数据,我在 Prompt 里写"请返回 JSON 格式",结果它经常返回"这是 JSON 格式的回答:{…}",或者字段名不对、类型不匹配。

后来发现 OpenAI 有专门的 JSON 模式:

def get_weather_info(location):
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {
                "role": "system",
                "content": "你是一个天气信息提取助手。从用户输入中提取地点信息。"
            },
            {"role": "user", "content": location}
        ],
        response_format={"type": "json_object"}
    )
    return response.choices[0].message.content

但 JSON 模式只是保证格式,不保证内容符合预期。更可靠的方案是加上 Pydantic 模型:

from pydantic import BaseModel
from typing import Optional

class WeatherQuery(BaseModel):
    location: str
    date: Optional[str] = None
    temperature_unit: str = "celsius"

def extract_weather_query(text: str) -> WeatherQuery:
    schema = WeatherQuery.model_json_schema()
    prompt = f"""
    从以下文本中提取天气查询信息,返回符合以下 JSON Schema 的结果:
    {schema}

    文本:{text}
    """

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "你是一个精确的信息提取助手。"},
            {"role": "user", "content": prompt}
        ],
        response_format={"type": "json_object"}
    )

    return WeatherQuery.model_validate_json(
        response.choices[0].message.content
    )

用了一段时间后发现,简单场景下用 JSON 模式就够了,复杂场景还是得在 Prompt 里加示例。有个项目需要从技术文档里提取 API 定义,JSON 模式经常返回字段缺失,后来在 Prompt 里加了 2-3 个例子,准确率明显提升。

从对话到工具调用

2023 年底 OpenAI 推出 Function Calling(后来改名成 Tools),这个问题才算有了一个标准方案。之前我试过在 Prompt 里教 AI"如果用户问天气,就返回 WEATHER:地点",但稳定性很差,经常该调用时不调用,不该调用时又瞎调用。

用 Function Calling 的版本:

import json

def get_current_weather(location, unit="celsius"):
    # 这里应该是实际的天气 API 调用
    return f"{location} 当前温度 25{unit[0]}"

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "获取指定地点的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市名称,例如:北京、上海"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位"
                    }
                },
                "required": ["location"]
            }
        }
    }
]

def run_conversation():
    messages = [
        {"role": "user", "content": "北京今天天气怎么样?"}
    ]

    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tools
    )

    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls

    if tool_calls:
        # 执行工具调用
        for tool_call in tool_calls:
            if tool_call.function.name == "get_current_weather":
                function_args = json.loads(tool_call.function.arguments)
                weather_info = get_current_weather(
                    function_args.get("location"),
                    function_args.get("unit", "celsius")
                )

                messages.append(response_message)
                messages.append({
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": tool_call.function.name,
                    "content": weather_info
                })

        # 获取最终回答
        second_response = client.chat.completions.create(
            model="gpt-4",
            messages=messages
        )
        return second_response.choices[0].message.content

print(run_conversation())

但实际项目中,工具调用不是这么简单。几个常见问题:

  1. 工具定义不规范:description 写得不清楚,AI 就不知道什么时候该调用;参数定义太松,传进来的值就各种离谱。

  2. 错误处理:工具调用可能失败(网络问题、API 限流、参数错误),这时候要告诉 AI 出了什么错,让它决定是重试还是改方案。

  3. 并发调用:如果用户的问题需要查多个独立信息(比如同时查多个城市的天气),串行调用就慢了。

实际项目里的版本会复杂一些:

from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Any

class ToolExecutor:
    def __init__(self):
        self.tools = {}  # name -> function

    def register(self, name: str, description: str, parameters: Dict[str, Any]):
        def decorator(func):
            self.tools[name] = {
                "function": func,
                "description": description,
                "parameters": parameters
            }
            return func
        return decorator

    def to_openai_format(self):
        return [
            {
                "type": "function",
                "function": {
                    "name": name,
                    "description": tool["description"],
                    "parameters": tool["parameters"]
                }
            }
            for name, tool in self.tools.items()
        ]

    def execute(self, tool_call) -> str:
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)

        try:
            result = self.tools[name]["function"](**args)
            return json.dumps({"success": True, "result": result})
        except Exception as e:
            return json.dumps({"success": False, "error": str(e)})

executor = ToolExecutor()

@executor.register(
    name="get_current_weather",
    description="获取指定地点的当前天气",
    parameters={
        "type": "object",
        "properties": {
            "location": {
                "type": "string",
                "description": "城市名称,例如:北京、上海"
            }
        },
        "required": ["location"]
    }
)
def get_current_weather(location: str) -> Dict[str, Any]:
    # 实际项目里这里会是真实的 API 调用
    return {"location": location, "temperature": 25, "condition": "晴"}

def run_agent_conversation(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]

    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=executor.to_openai_format()
    )

    response_message = response.choices[0].message

    while response_message.tool_calls:
        messages.append(response_message)

        # 并发执行所有工具调用
        with ThreadPoolExecutor() as executor_pool:
            for tool_call in response_message.tool_calls:
                executor_pool.submit(
                    lambda tc: messages.append({
                        "tool_call_id": tc.id,
                        "role": "tool",
                        "name": tc.function.name,
                        "content": executor.execute(tc)
                    }),
                    tool_call
                )

        # 再次请求,获取下一步
        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=executor.to_openai_format()
        )
        response_message = response.choices[0].message

    return response_message.content

Prompt 优化:从模糊到精确

刚开始写 Prompt 时很随意,“你是一个有用的助手”、“帮我写一段代码"这种写法很常见。后来发现 Prompt 的质量直接影响结果,尤其是工具调用场景。

几个关键经验:

  1. 身份定义要具体:不要说"你是一个编程助手”,要说"你是一个专注于 Python 后端开发的工程师,熟悉 FastAPI、SQLAlchemy 和 PostgreSQL"。

  2. 约束条件要具体:除了"别生成错误代码",还要写清楚"不确定 API 用法就说不知道,别编"。

  3. 输出格式要示例:尤其是结构化输出,给 1-2 个完整例子比写一堆说明管用。

实际项目里常用的 Prompt 模板:

SYSTEM_PROMPT_TEMPLATE = """
你是一个专业的{role},专注于{expertise_area}
## 你的职责
{responsibilities}

## 回答要求
1. 如果信息不足,明确提问而不是猜测
2. 如果不确定某个细节,标注出来
3. 代码示例要完整可运行,包含必要的 import 和错误处理
4. 解释技术选择时说明理由

## 不可做
- 不要编造不存在的库或 API
- 不要给出看起来正确但实际无法运行的代码
- 不要忽略用户明确提到的限制条件

{additional_constraints}
"""

def get_system_prompt(role="后端工程师", expertise_area="Python Web 开发"):
    return SYSTEM_PROMPT_TEMPLATE.format(
        role=role,
        expertise_area=expertise_area,
        responsibilities="\n".join([
            "提供清晰、准确的解决方案",
            "考虑性能、安全性和可维护性",
            "给出完整可用的代码示例"
        ]),
        additional_constraints="所有代码示例使用 Python 3.11+ 语法。"
    )

但 Prompt 不是越长越好。后来发现,核心还是"描述清楚你想要什么",而不是堆一堆规则。有时候一段简洁的例子比十条规则管用。

错误处理:从裸调到容错

最早的代码基本没错误处理,API 一报错前端就白屏。后来慢慢加上重试、降级、兜底。

几个关键点:

  1. 网络问题:超时、连接失败要重试,但要有限次数和退避策略。
  2. API 限流:OpenAI 有速率限制,达到限制要等一段时间。
  3. 内容安全:可能触发内容审核,这时候要优雅降级。
  4. 模型故障:偶尔会遇到模型返回异常格式或空内容。

实际项目里的版本:

import time
from tenacity import retry, stop_after_attempt, wait_exponential
from openai import RateLimitError, APITimeoutError

class RobustGPTClient:
    def __init__(self, api_key: str, max_retries=3):
        self.client = OpenAI(api_key=api_key)
        self.max_retries = max_retries

    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10)
    )
    def chat_completion(self, messages, model="gpt-4"):
        try:
            response = self.client.chat.completions.create(
                model=model,
                messages=messages,
                timeout=30  # 设置超时
            )
            return response.choices[0].message.content

        except RateLimitError as e:
            # 速率限制,等待后重试
            wait_time = int(e.response.headers.get('retry-after', 5))
            time.sleep(wait_time)
            raise  # 触发重试

        except APITimeoutError:
            # 超时,直接重试
            raise

        except Exception as e:
            # 其他错误,记录并返回兜底响应
            print(f"API 调用失败: {e}")
            return "抱歉,我现在无法回答,请稍后再试。"

    def safe_chat(self, messages, fallback_response="我遇到一些问题,无法继续。"):
        try:
            return self.chat_completion(messages)
        except Exception as e:
            print(f"所有重试均失败: {e}")
            return fallback_response

还有一类错误是"模型胡说八道",这个没法靠代码完全避免,但可以在设计上降低影响:比如让它标明不确定的内容,或者对于关键结果加人工审核。

从聊天机器人到智能助手

做到这一步,能对话、有记忆、能调用工具、能处理错误,基本上算一个"能干活"的智能助手了。但实际项目里,还有几个关键区别:

  1. 任务分解:复杂任务需要分解成多个步骤,每个步骤可能需要不同的工具。
  2. 状态管理:长时间运行的任务需要保存中间状态,支持恢复和回滚。
  3. 用户反馈:允许用户对结果进行修正,形成闭环。

这些能力就超出单纯的"对话"范畴了,接近于"Agent"的领域。我摸索了一个简化版的任务框架:

from enum import Enum
from dataclasses import dataclass
from typing import List, Optional

class TaskStatus(Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    FAILED = "failed"

@dataclass
class TaskStep:
    id: str
    description: str
    status: TaskStatus = TaskStatus.PENDING
    result: Optional[str] = None
    error: Optional[str] = None

class TaskAgent:
    def __init__(self, gpt_client, tool_executor):
        self.gpt_client = gpt_client
        self.tool_executor = tool_executor
        self.current_task = None

    def plan_task(self, user_request: str) -> List[TaskStep]:
        # 让 GPT 分解任务
        prompt = f"""
        将以下用户请求分解为具体步骤,每个步骤要能明确执行或验证:

        用户请求:{user_request}

        返回格式(JSON):
        {{"steps": [{{"id": "1", "description": "步骤描述"}}, ...]}}
        """

        response = self.gpt_client.chat_completion([
            {"role": "system", "content": "你是一个任务规划专家。"},
            {"role": "user", "content": prompt}
        ])

        steps_data = json.loads(response)
        return [
            TaskStep(id=step["id"], description=step["description"])
            for step in steps_data["steps"]
        ]

    def execute_step(self, step: TaskStep, context: dict) -> str:
        # 执行单个步骤
        prompt = f"""
        执行以下任务步骤:

        步骤描述:{step.description}

        上下文信息:{json.dumps(context, ensure_ascii=False)}

        如果需要调用工具,使用提供的工具;如果信息不足,明确说明需要什么信息。
        """

        response = self.gpt_client.chat_completion([
            {"role": "system", "content": "你是一个任务执行专家。"},
            {"role": "user", "content": prompt}
        ], tools=self.tool_executor.to_openai_format())

        return response

    def run_task(self, user_request: str) -> dict:
        # 规划任务
        steps = self.plan_task(user_request)
        context = {"user_request": user_request}

        results = []
        for step in steps:
            step.status = TaskStatus.IN_PROGRESS

            try:
                result = self.execute_step(step, context)
                step.status = TaskStatus.COMPLETED
                step.result = result
                context[f"step_{step.id}_result"] = result
            except Exception as e:
                step.status = TaskStatus.FAILED
                step.error = str(e)
                # 简化处理:遇到错误就停止
                break

            results.append(step)

        return {
            "request": user_request,
            "steps": results,
            "status": "completed" if all(s.status == TaskStatus.COMPLETED for s in results) else "failed"
        }

这个框架很粗糙,实际项目里会更复杂,但基本思路是一样的:把复杂任务分解成步骤,逐个执行,每个步骤的状态和结果都被记录下来。

几个踩过的坑

最后说几个印象比较深的坑:

  1. Token 计算问题:tiktoken 的计算和 OpenAI 实际计费不完全一致,最好留一些余量。另外,工具调用的参数和返回值也算 Token,这个容易忽略。

  2. 并发安全问题:多用户同时调用同一个 GPT 客户端实例,可能会有问题。后来改成每个请求独立实例,或者用连接池。

  3. 模型升级陷阱:从 gpt-3.5 升级到 gpt-4 后,某些 Prompt 效果变差了,需要重新调优。模型不是越新越好,要看具体场景。

  4. 成本控制:早期没做成本统计,测试用户刷了几百次对话才发现超预算。后来加上每次调用的 Token 数和成本记录。

  5. 响应时间:gpt-4 的响应时间比 gpt-3.5 慢很多,用户体验明显下降。对于实时性要求高的场景,可能需要降级或缓存。

收尾

从一行 chat() 脚本到能调工具、管记忆、做任务分解,变化最大的认知是:让 AI 在真实场景里稳定干活,比学会调 API 难得多。

简单场景现在基本能 hold 住;复杂任务、长链路、多用户并发,边界还是一堆。技术迭代快,很多"最佳实践"隔半年就得重审。

我的做法一直是从小场景开跑,用真实用户反馈驱动迭代。有些问题不是 Prompt 写得差,就是当前模型能力到那了。第一次写的聊天脚本现在看很简陋,但后面所有改动都从那几行代码长出来的。

版权声明: 本文首发于 指尖魔法屋-关于GPT应用的几点记录https://blog.thinkmoon.cn/post/167-gpt-app-deep-dive-from-chatbot-to-intelligent-assistant/) 转载或引用必须申明原指尖魔法屋来源及源地址!