简介:这是一份由Olivier Caelen与Marie-Alice Blete合著、O'Reilly Media出品的英文技术电子书,围绕GPT-4与ChatGPT讲解如何构建智能聊天机器人、内容生成器及应用,面向希望将大语言模型引入实际产品开发的中高级开发者与算法工程师。当前展示的是Early Release版本,开篇从GPT-4与ChatGPT的核心基础切入,逐步拆解模型能力边界、交互方式与开发环境准备,帮助读者在正式版出版前尽早掌握相关技术脉络。全书以第一章“GPT-4 and ChatGPT Essentials”为起点,系统介绍必备知识,后续章节则聚焦应用开发路径,适合作为从理论认知过渡到动手实践的桥梁读物。资源包内共1个文件,类型为PDF文档,整体体积仅2.86MB,文件紧凑便于移动端或离线阅读;目前已有1133人学习或下载,印证了其在开发者群体中的参考价值。读者可在早期版本中快速获得大模型应用开发的全局视图与设计启发,包括如何组织对话流程、选择合适的模型能力以及规划智能化功能落地,为自建ChatGPT类应用打下扎实基础。
1. 慢着,这不止是一本 API 手册那么简单
不少开发者拿到《Developing Apps with GPT-4 and ChatGPT》这份资料后,第一反应是把它当成打交道用的大模型接口文档来查。带着这个预期去读,通常第一遍就会失望:里面没有完整的端点清单,没有标准化的错误码表,通篇更像是一位老工程师在讲——一个真正可用的 GPT-4 应用,是用什么样的骨架把对话能力组织起来的。它真正要解决的是"能聊天"和"能当应用"之间的那道设计鸿沟:谁在主控、谁来记状态、模型输出怎么变成系统里真实发生的动作。适合的是那些已经试过提示词、发现单靠调话术总在原地打转,正想把原型往生产推的开发者。读它之前,最好先把心态切到"做工程设计",而不是"查字典"。
2. 把聊天变成应用:GPT-4 应用的主控模型与最小骨架
2.1 为什么"对话编排"是应用骨架,而不是提示词的事
一个常见误用是:觉得 GPT-4 应用 = 把提示词写长一点 + 把用户问题丢进去 + 展示返回结果。这种"单发请求"的写法,做 Demo 可以,做真实功能马上会撞上三堵墙。第一堵是状态墙:用户说"把刚才那个订单取消",应用根本不知道"刚才那个"指哪个订单,因为每次请求都是独立的,上下文在你的代码里而不在模型里。第二堵是权限墙:模型只能输出文字,它没法真的去查库存、改数据库、发通知,所有"动作"都得由你的代码执行。第三堵是成本墙:一个像样的业务流程,光靠提示词把所有规则塞进一次请求,Token 消耗会膨胀到没法商用。
所以翻开这本书,真正值得先吸收的是一句话:GPT-4 只是你系统里的一个组件,它有输入输出,有擅长的推理,但它不拥有你的业务状态。应用的主控权在你自己写的编排层里。编排层负责决定什么时候把请求发给模型、带哪些上下文、模型回复之后要执行什么本地逻辑;模型负责的是其中"理解和生成"这一段。把这个边界划清楚,后面所有的参数调整、函数调用、上下文管理才有的放矢。
我一般会在项目里先画一张数据流草图:用户输入先进路由,路由判断走哪个子任务;子任务决定怎么组装消息;组装后的消息发给模型;模型输出交给一个解释器,解释器转成具体的函数调用。这张图画完,代码写起来就是填空。下面这个最小骨架,就是把这张草图落成可运行代码的第一步。
2.2 一个能直接跑起来的 Python 最小应用:请求封装与参数说明
# 该示例用于演示 GPT-4 应用的最小请求骨架,请替换为实际可用的配置 import os import json import requests # 所有连接信息从环境变量读取,避免硬编码密钥 API_BASE = os.environ.get("GPT_API_BASE") # 接口入口地址 API_KEY = os.environ.get("GPT_API_KEY") # 访问密钥 MODEL = os.environ.get("GPT_MODEL", "gpt-4") # 模型名,默认取 gpt-4 def chat_once(messages, temperature=0.3, max_tokens=1024): """发送一次对话请求,返回消息内容。""" url = f"{API_BASE}/chat/completions" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } payload = { "model": MODEL, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } resp = requests.post(url, headers=headers, json=payload, timeout=(5, 30)) resp.raise_for_status() # 非 2xx 直接抛异常,方便上层感知 data = resp.json() return data["choices"][0]["message"]["content"] # 带上少量历史消息的调用方式 if __name__ == "__main__": messages = [ {"role": "system", "content": "你是一个订单助手,回答要简短。"}, {"role": "user", "content": "我的订单 SZ20240101 什么时候发货?"}, ] reply = chat_once(messages) print(reply)这段代码做的事情很朴素:把消息列表发送出去,拿到完整的返回内容。但它把几个关键参数暴露出来了。temperature控制随机性,值越低输出越确定,订单查询这类功能我通常压在 0.2 附近,文案创作再放到 0.7 以上。max_tokens是输出上限,不是整体上限,它直接影响成本和响应时间,宁可设小一点再截断提示,也不要让它无节制地生成。timeout=(5, 30)把连接超时和读取超时分开,前者 5 秒,后者 30 秒,避免网络抖动时请求长时间挂起。
messages是结构的核心。它由 system、user、assistant 三类消息组成,system 消息定义模型的角色和行为边界,user 是本次输入,assistant 历史消息则负责把之前的对话语境带回来。注意,这里每个消息都带role字段,API 靠这个字段判断对话顺序,乱序会导致上下文错乱。另外,历史消息里如果含有已执行动作的结果,应该单独用一段拼接进去,比如"系统已取消订单 #1234",而不是只放一句模型说过的"好的"。
2.3 意图路由与上下文管理:让一次对话干多件事
最小骨架跑通之后,下一步是处理"用户什么都会问"的现实。不能把所有话都丢给一个 system 消息去管,那样提示词会越写越长、互相打架。常见做法是加一层意图路由,先判断用户来干什么,再决定用哪套提示词和哪套工具。这一步可以用关键词规则做,也可以用模型做,规则优先,因为规则可控、零成本。
# 意图路由:先用简单规则快速分流,再考虑是否引入模型判断 INTENT_RULES = [ ("查订单", ["订单", "物流", "发货", "快递"]), ("退换货", ["退货", "换货", "退款", "售后"]), ("咨询", ["怎么", "什么", "如何", "?"]), ] def route(intent_text): """返回命中的意图名称,未命中返回默认值。""" for intent, keywords in INTENT_RULES: if any(kw in intent_text for kw in keywords): return intent return "default" def build_prompt(intent): """不同意图使用不同的 system 指令与上下文策略。""" prompt_map = { "查订单": "只回答订单状态与物流信息,不确定时直接说明。", "退换货": "引导用户确认订单号,先解释退换流程再收集信息。", "咨询": "回答产品相关问题,涉及价格时要提示可能变化。", } return prompt_map.get(intent, "你是通用助理,回答简洁。")路由做对之后,上下文管理就有了边界:查订单的对话,只把订单表和物流消息拼进去;退换货的对话,只带售后政策。不同意图之间不要共享消息历史,否则模型会被无关内容干扰,Token 也会浪费。这里有个细节:判断完意图之后,build_prompt返回的 system 消息要放在消息列表的第一位,并且历史消息只保留最近 N 轮,超出部分用一句摘要代替。摘要怎么写也有讲究,不要写"用户问了发货时间",而要写"用户等待订单 SZ20240101 的发货确认",保留关键实体。
到这里,骨架已经能处理"多类任务 + 多轮对话"了。但此时模型还只能说话,不能做事。真正让它从"聊天机器人"变成"应用"的,是下一步要给模型装上的工具调用能力。
3. 给模型装上"手":功能调用是应用开发的分水岭
3.1 为什么功能调用改变了应用写法
在功能调用出现之前,让模型执行动作的常见路子是:让模型输出一段约定格式的文本,比如"调用函数:check_order,参数:{'order_id': '123'}",然后再写正则去解析。这个方案在演示环境能用,一上真实业务就摇摇欲坠。模型偶尔会在括号里多加一个引号、把字段名缩写、或者直接说"我要调用函数了但是忘了参数",任何一个小偏差都会让解析器静默失败,然后用户收到"系统繁忙"。
功能调用机制相当于把"让模型声明自己要用什么工具、传什么参数"这件事,从文本协议升级成了结构化协议。模型输出的不是自然语言,是带类型标记的函数名和参数列表,你的代码只需要解析这个结构、执行对应函数、把结果回填给模型。这样一来,解析失败率大幅下降,"模型 → 代码"这条链路的可靠性问题是靠协议本身解决的,不是靠更灵活的提示词碰运气。
但这套机制不是自动生效的,你要做三件事:定义工具清单、把清单随请求发出、处理模型回传的调用请求。工具清单本质上是 JSON Schema,字段名、类型、必填项写清楚,模型才能按规范生成参数。下面用一个订单查询工具走一遍。
3.2 注册工具与解析请求:一个订单查询的工具示例
# 工具定义:一个订单查询函数 TOOLS = [ { "type": "function", "function": { "name": "query_order", "description": "根据订单号查询订单状态与物流信息", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为字母 SZ 加数字", } }, "required": ["order_id"], }, }, } ] def query_order(order_id): """实际执行查询的本地函数,返回结果给模型。""" # 这里应该是查数据库或调内部服务,演示用静态数据 if order_id == "SZ20240101": return {"status": "shipped", "eta": "2024-02-01"} return {"status": "not_found"} def run_conversation(messages): """发送携带工具定义的请求,处理模型的调用请求。""" payload = { "model": MODEL, "messages": messages, "tools": TOOLS, "tool_choice": "auto", # 让模型自行决定是否需要调用工具 } resp = requests.post(f"{API_BASE}/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json=payload, timeout=(5, 30)) return resp.json()注意tool_choice这个参数。设成"auto"表示模型按需决定;如果你希望某次对话必须使用某个工具,可以直接写{"type": "function", "function": {"name": "query_order"}}。description字段要尽量写清楚工具的使用边界,模型是靠它来匹配用户意图的,写"根据订单号查询"比写"查询函数"好用得多。参数 schema 里required不要滥用,确有必要才填,否则模型为了凑齐参数可能会编造值。
响应里如果模型确实想调用工具,会多出一个tool_calls字段,内容包含工具名和参数对象。此时message.content通常是空的,你的代码看到tool_calls就必须转向执行分支,而不要试图去读自然语言回复。
3.3 完整闭环:执行本地函数并回填结果
# 处理工具调用并回填,形成"模型请求 -> 代码执行 -> 结果返回"闭环 def execute_tool_calls(message, messages): """解析模型返回的工具调用,执行本地函数,并把结果追加到消息历史。""" if not message.get("tool_calls"): return messages messages.append({"role": "assistant", "content": None, "tool_calls": message["tool_calls"]}) for call in message["tool_calls"]: func_name = call["function"]["name"] raw_args = json.loads(call["function"]["arguments"]) if call["function"].get("arguments") else {} print(f"[exec] {func_name}({raw_args})") # 本地日志,便于排查 if func_name == "query_order": result = query_order(raw_args.get("order_id")) else: result = {"error": f"unknown function: {func_name}"} messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False), }) return messages这段代码的核心有两处。一处是把助手消息带着tool_calls原样放回历史,这是协议要求的,乱改会报格式错误;另一处是每条工具结果都要带上tool_call_id,这个 ID 来自模型给你的调用请求,保证结果和调用是一一对应的。回填之后,要把整套messages再发一次请求,模型会读取工具返回的 JSON,组织成给用户的自然语言回复。所以一次带工具的操作,实际上会发起两轮请求:第一轮拿到调用意图,第二轮拿到最终答复。
边界在哪儿?一是所有本地函数必须快速返回,如果查询要几秒,建议先给模型一个"查询中"的中间状态,再异步回填,而不是阻塞整个对话。二是函数执行要放在沙箱环境里,别让模型能直接调用删除或写库的裸函数,一定要包一层校验,比如检查订单号格式、核对当前用户是否有权限。三是工具数量控制在十个以内,太多的工具定义会吃掉大量上下文空间,而且模型容易选错工具,必要时做分组的做法是:先路由到业务域,再给该业务域单独传一组工具。
到这里,一个完整的"带手"应用已经成型了。但细节里藏着的坑,往往是在开发环境欢快地跑通、到了生产就原形毕露的。下面是我建议你在上生产之前,逐条对着排查的五个场景。
4. 上生产前先排雷:GPT-4 应用最容易翻车的 5 个场景
4.1 上下文无限膨胀:账单和延迟是怎么失控的
现象:应用刚上线正常,跑了几天后响应越来越慢,费用曲线陡增,偶尔报"上下文超长"错误。原因:代码把用户每次对话的原始消息全部追加进messages,从不清理。会话越长,每次请求带的 Token 越多,模型处理时间随之变长,费用按输入 Token 计费,自然水涨船高。
解决:给会话设置窗口策略。保留最近 6 轮对话原文,超出部分让模型生成一条摘要塞进系统消息,摘要专注于保留业务关键信息:订单号、用户意图、已确认的前提条件、待办事项。另一种做法是超过窗口直接截断并提醒用户"较早的消息已被压缩",多数场景下用户是可以接受的。关键是这个逻辑要写在会话服务里,而不是靠调用方自觉。
4.2 同一个提示词,换了模型版本就翻车
现象:开发环境用 A 版本模型调试好的一套提示词,部署到线上发现输出格式变了,原本稳定的 JSON 结构开始漏字段。原因:模型服务方会迭代版本,行为随之微调,你的提示词越苛刻、对格式要求越细,版本变更的冲击就越大。这属于"黑匣子"变化,不是你的代码逻辑出问题。
解决:把提示词里对格式的约束从"描述性要求"改成"示例性要求"。不只说"返回 JSON",要给一个完整的字段示例,并注明"严格按此结构,不要添加额外字段"。输出端再加一道校验层,解析失败就重试一次,并携带上次的错误信息让模型修正——这个自纠错流程比把提示词写到天花乱坠有用得多。
4.3 超时重试后的重复动作
现象:用户在界面上点了一次"取消订单",等了十几秒没反应,又点了一次,结果产生两个取消请求。原因:网络超时后客户端重试,但第一次请求其实已经到达服务端并执行了,只是响应没能及时返回。模型这边没有幂等概念,同样的指令执行两次,业务端就被重复下单或重复取消了。
解决:请求里带一个幂等键,比如用会话 ID 加操作序号生成;本地函数执行前先查幂等表,已执行过就直接返回上次结果。这是一条事后补救也基本来不及的坑,必须在设计工具调用时就预留request_id参数。像取消、转账这类敏感操作,我还会在函数里加二次确认参数,要求模型先确认用户意愿再执行,进一步降低误操作概率。
4.4 让模型输出 JSON,结果却不是合法 JSON
现象:应用逻辑要求模型只输出结构化数据,但它偶尔给你带```json标记,或者夹杂一句"下面是你要的数据"再贴上 JSON。原因:模型生成的是文本,不是数据对象。json.loads要求字符串百分之百合法,多一个字符都会整个失败。
解决:优先用函数调用机制拿到结构化参数,它走的是协议通道,天然规避这个问题。如果一定要解析自然语言里的 JSON,先用正则把代码围栏剥掉,再定位第一个{和最后一个}截取片段,最后解析。工具类 JSON 字段固定的话,还可以用 pydantic 这类校验库做模型,字段缺失时给出默认值,而不是直接让整个请求失败。
4.5 拿真实 API 当回归测试环境
现象:测试用例的断言经常飘,同样输入有时通过有时失败,测试跑完还产生一笔不小的费用。原因:模型输出有随机性,直接拿真实接口做断言等于把非确定性服务当成确定性服务来测,再加上每次回归都在烧 Token。
解决:把模型调用封装成可替换的接口,日常开发与回归测试注入本地录制好的样本响应,只在发布前跑一轮真实链路做冒烟测试。回归用例聚焦两件事:路由是否命中正确意图、工具调用参数是否生成正确,用"语义断言"而非"字符串相等"。真实链路测试保留少数几条关键业务路径就够了,其余交给样本库。这条习惯能同时把测试稳定性和成本两个问题摁住。
5. 把开发成本打下来的最后一招:本地回归样本库
最后一招,是我在做过一个订单助手型应用之后沉淀下来的习惯:建一个本地回归样本库。它的思路是,把所有和模型交互的边界场景固化成本地文件,开发、联调、回归通通打在本地,只在发布前才走一次真实模型。这样做的直接好处是,测试不再受网络、额度和模型行为波动影响,跑十遍和跑一遍结果一模一样。
样本库的组织结构按场景分目录,每个场景放一对输入输出文件:输入存messages和工具定义,输出存模型响应或工具调用结果。跑回归时,把"消息组装 → 模型调用 → 工具执行 → 回填再请求"这条链路的模型调用部分替换成本地读取,其他逻辑全部照走。覆盖面优先选这些:单轮直接答复、多轮追问、工具调用成功、工具调用参数缺失、模型返回 JSON 夹杂多余文字、超长上下文触发截断。每个样本都对应一个曾经翻过车的现场,记录的意义就是防止再翻。
上面这个样本库的做法,配合一个小的本地校验脚本,把样本里的输出字段缺失、工具参数类型错误、幂等键重复这三类问题做成自动检查。每次改完提示词或路由逻辑,跑一遍全量样本,能看到哪些旧场景被"改坏了",再决定要不要收下这个变化。这套流程把模型应用开发从"靠感觉调提示词"变成了"批量验证、坏了我知道",对团队协作尤其有意义——新人接手也能通过样本快速理解系统行为。
我自己在这条路上栽过的最大跟头,就是刚上手时贪快,跳过样本库直接拿真实 API 调了一天,最后花在排查"到底是我参数错了还是模型抽风"上的时间,比重写一遍还多。从那以后所有 GPT-4 相关的项目都坚持本地优先测试,真实链路只做发布前的冒烟。项目越到后期,这套习惯省下的钱和时间就越明显。如果你现在正被"调好了又变、测了又飘"折磨,不妨先从录一个样本开始,把一次成功调用固化下来,再慢慢织成一张网。希望帮到你。
本文还有配套的精品资源,点击获取