AI Agent底层基石:单次模型请求的三种方式与选型实战

1. 为什么单次模型请求值得单独拎出来聊

很多人一上来就搞多轮对话、工具调用、记忆管理,结果连一次干净的模型请求都发不明白。我见过太多项目,Agent 框架搭得花里胡哨,最后排查问题发现是底层单次请求的参数传错了,或者流式和非流式的处理逻辑混在一起,导致整个链路雪崩。所以这次我想把“单次模型请求”这件事单独拆开讲透,因为它是所有 AI Agent 的地基,地基不平,上面盖什么都会歪。

所谓单次模型请求,说白了就是你的程序向大模型服务端发一次调用,拿回一次结果,中间不涉及多轮上下文拼接、不涉及工具回调、不涉及状态保持。听起来简单,但实际落地时有三种主流方式,每种方式的适用场景、坑点和性能特征完全不一样。选错了方式,轻则代码冗余难维护,重则线上超时率飙升、费用翻倍。

这篇文章适合谁看?如果你正在写 Agent 的底层调用层,或者你已经在用某些框架但想搞清楚它底层到底怎么发请求的,再或者你单纯想优化一下自己项目里模型调用的稳定性和成本,那这篇内容应该能帮你省下不少试错时间。我会把三种方式的原理、代码实现、参数计算、排查技巧全部摊开讲,尽量做到你看完就能直接抄作业。

2. 三种主流请求方式的全景拆解

2.1 先搞清楚“单次请求”到底包含哪些环节

在展开三种方式之前,得先把一次完整的模型请求拆解清楚。不管用什么方式,一次请求本质上都包含这几个阶段:构造请求体、建立连接、发送数据、等待响应、解析响应、异常处理。区别在于,不同方式对这些阶段的控制粒度不一样。

构造请求体这一步,核心是消息列表的组装。单次请求通常只包含一条 system 消息和一条 user 消息,但即使是这么简单的结构,也有讲究。比如 system 消息放什么内容、user 消息要不要做预处理、需不需要加 few-shot 示例,这些都会直接影响返回质量。我个人的习惯是,单次请求的 system 消息尽量精简,把角色定义和输出格式约束写清楚就行,不要把多轮对话才需要的东西塞进来。

连接阶段涉及的是网络层面的东西。同步请求会阻塞当前线程直到拿到完整响应,异步请求则把控制权交还出去,流式请求会在连接建立后持续接收数据块。这三种模式对应到具体的请求方式上,就衍生出了下面要讲的三种主流方案。

2.2 三种方式的定义与核心差异

第一种是原生 HTTP 同步请求。你直接用 HTTP 客户端库向模型服务的接口地址发一个 POST 请求,body 里带上模型名称、消息列表、温度等参数,然后等服务器返回完整的 JSON 响应。这种方式最原始,但也最透明,没有任何框架封装,出问题了你能精确知道是哪一层的事。

第二种是 SDK 异步请求。主流模型服务商都会提供官方 SDK,里面封装了异步客户端。你用 async/await 语法发起调用,底层走的是异步 IO,不会阻塞事件循环。这种方式在 Python 的 asyncio 生态或者 Node.js 的 Promise 体系里非常常见,适合需要并发发起多个单次请求的场景。

第三种是流式请求。同样是发一次请求,但服务端会以 Server-Sent Events 或者 chunked transfer 的方式逐块返回内容。你在客户端逐块接收、逐块处理,用户能看到文字一个个蹦出来。这种方式严格来说也是单次请求,只不过响应的接收方式从“一次性拿完”变成了“边拿边处理”。

这三种方式没有绝对的优劣,关键看你的场景。下面我逐个展开,把每种方式的实现细节和踩坑经验都讲清楚。

3. 原生 HTTP 同步请求:最笨但最可控的方式

3.1 适用场景与选型理由

原生 HTTP 同步请求最适合什么场景?我的经验是:脚本类任务、离线批处理、调试阶段、以及对第三方依赖极度敏感的生产环境。为什么?因为这种方式不依赖任何厂商 SDK,你只需要一个通用的 HTTP 客户端库就行。哪怕服务商的 SDK 更新了、出 bug 了、或者你用的语言根本没有官方 SDK,你照样能发请求。

举个例子,我之前做过一个数据标注清洗的小工具,需要把几千条文本逐条发给模型做分类。这种场景下,我不需要流式输出,不需要并发(因为要控制速率),也不需要复杂的错误重试框架。直接用 HTTP 同步请求,写个循环,每条请求之间加个 sleep,简单粗暴但极其稳定。跑了一晚上,一条没丢。

另一个典型场景是跨语言调用。比如你的主业务是 Java 写的,但模型服务商的 SDK 只有 Python 和 Node.js 版本。这时候用 Java 的 HTTP 客户端直接发请求,反而是最省事的方案。你只需要照着 API 文档拼 JSON 就行,不需要为了调个模型再引入一个 Python 微服务。

3.2 请求构造的关键参数与计算过程

构造一个同步请求,核心参数其实就那么几个,但每个都值得细说。我拿一个典型的请求体来举例:

{ "model": "某模型名称", "messages": [ {"role": "system", "content": "你是一个文本分类助手,只输出类别标签。"}, {"role": "user", "content": "帮我判断这条评论的情感倾向:这个产品用起来还不错。"} ], "temperature": 0.1, "max_tokens": 64, "top_p": 0.9 }

temperature 设 0.1 是因为分类任务需要确定性输出,温度越低模型越倾向于选概率最高的词。max_tokens 设 64 是因为分类结果只需要输出一个标签,给太多 token 纯属浪费。top_p 设 0.9 是配合低温度做一点微调,避免完全死板。

这里有个计算过程值得展开:max_tokens 到底设多少合适?我的经验公式是:预期输出长度的 1.5 倍,再向上取整到 16 的倍数。比如你预期输出 20 个字,那 20 乘以 1.5 等于 30,向上取到 32。为什么要留 1.5 倍余量?因为模型有时候会输出一些额外的解释性文字,虽然你在 prompt 里约束了只输出标签,但它偶尔还是会多嘴。留点余量避免被截断,截断了的响应处理起来更麻烦。

还有一个容易被忽略的参数是超时时间。同步请求必须设超时,而且不能设太长。我一般设 30 秒,超过 30 秒没返回就直接断开重试。为什么不设 60 秒?因为模型服务端的排队时间加上推理时间,正常情况下不会超过 30 秒。如果超过了,说明服务端可能出问题了,你等再久也没用,不如早点断开走重试逻辑。

3.3 完整代码实现与逐行说明

下面是一个用 Python 的 requests 库实现的同步请求示例,我逐段解释:

import requests import json import time def single_request_sync(prompt, system_msg="你是一个助手。", max_retries=3): url = "https://api.example.com/v1/chat/completions" headers = { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY" } payload = { "model": "某模型名称", "messages": [ {"role": "system", "content": system_msg}, {"role": "user", "content": prompt} ], "temperature": 0.1, "max_tokens": 128 } for attempt in range(max_retries): try: resp = requests.post(url, headers=headers, json=payload, timeout=30) if resp.status_code == 200: data = resp.json() return data["choices"][0]["message"]["content"] elif resp.status_code == 429: wait = 2 ** attempt time.sleep(wait) continue else: print(f"请求失败,状态码:{resp.status_code},响应:{resp.text}") return None except requests.exceptions.Timeout: print(f"第 {attempt + 1} 次请求超时") continue except requests.exceptions.RequestException as e: print(f"网络异常:{e}") continue return None

这段代码有几个细节值得说。第一,重试逻辑里对 429 状态码做了指数退避,第一次等 1 秒,第二次等 2 秒,第三次等 4 秒。这是应对速率限制的标准做法,比固定间隔重试要优雅得多。第二,超时异常和网络异常分开捕获,因为它们的处理策略可能不同。超时通常重试就行,网络异常可能需要检查代理或者 DNS 配置。第三,返回 None 而不是抛异常,是因为调用方通常只需要知道“这次没拿到结果”,不需要处理复杂的异常链。

注意:API Key 千万不要硬编码在代码里。我见过太多项目把 Key 写在源码里然后提交到了代码仓库,结果被人扫到之后一夜之间跑掉几千块。正确做法是用环境变量或者密钥管理服务。

3.4 实操心得与常见坑

用同步请求最大的坑就是阻塞。如果你在一个 Web 服务的主线程里发同步请求,那这个请求没返回之前,整个线程就被卡住了。并发量一上来,线程池瞬间打满,服务直接不可用。所以同步请求只适合脚本、离线任务、或者你明确知道并发量很低的场景。

第二个坑是连接复用。requests 库默认每次请求都新建连接,如果你在循环里发几千次请求,每次都要做 TCP 三次握手和 TLS 握手,开销很大。解决办法是用 requests.Session(),它会在底层维护一个连接池,复用已经建立的连接。实测下来,用 Session 之后,同样数量的请求,总耗时能降低 30% 到 40%。

第三个坑是响应解析的健壮性。不要假设返回的 JSON 结构永远符合预期。我遇到过服务端在高峰期返回了一个缺少 choices 字段的响应,结果代码直接 KeyError 崩了。后来我加了一层防御性解析,先检查 choices 是否存在、是否为空、message 里是否有 content,全部确认之后再取值。多写这几行判断,能省掉很多半夜被叫起来修 bug 的时间。

4. SDK 异步请求:高并发场景下的效率利器

4.1 异步请求的核心原理与优势

异步请求的本质是把“等待网络响应”这段时间利用起来。同步模式下,线程发出请求后就傻等,什么也干不了。异步模式下,事件循环在等待响应的同时可以去处理其他任务,等响应回来了再切回来继续处理。对于单次模型请求这种 IO 密集型操作,异步带来的吞吐量提升是非常明显的。

我做过一个对比测试:同样是用 100 个单次请求去处理 100 条文本,同步方式串行执行耗时大约 180 秒,异步方式并发执行耗时不到 8 秒。差了 20 多倍。当然这是在服务端不限制并发的前提下,实际使用中还要考虑速率限制,但异步的收益依然巨大。

SDK 异步请求和原生 HTTP 异步请求的区别在于,SDK 帮你封装了连接池管理、重试策略、参数校验、响应解析这些脏活累活。你只需要关注业务逻辑,不用操心底层细节。代价是你得依赖这个 SDK,如果 SDK 有 bug 或者更新不及时,你就得等或者自己 fork 一份改。

4.2 异步客户端的初始化与参数配置

以 Python 生态中常见的异步 SDK 用法为例,初始化通常长这样:

import asyncio from some_sdk import AsyncClient client = AsyncClient( api_key="YOUR_API_KEY", base_url="https://api.example.com/v1", timeout=30.0, max_retries=3, connection_pool_size=20 )

这里有几个参数需要根据实际情况调整。connection_pool_size 是连接池大小,设太小了并发上不去,设太大了浪费资源。我的经验值是:预期并发数乘以 1.5。比如你预计同时最多发 10 个请求,那连接池设 15 就够了。timeout 和 max_retries 的含义和同步请求一样,但异步环境下超时的处理更复杂一些,因为可能涉及多个协程同时超时。

还有一个容易忽略的参数是并发限制。很多 SDK 不直接提供并发限制参数,你需要自己用信号量来控制。比如:

semaphore = asyncio.Semaphore(10) async def limited_request(prompt): async with semaphore: return await client.chat.completions.create( model="某模型名称", messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=128 )

这个信号量保证了同时最多只有 10 个请求在飞,超出的请求会排队等待。为什么要做这个限制?因为服务端通常有并发上限,你一次性发 100 个请求过去,大部分会被拒绝或者排队,反而降低了整体效率。用信号量控制在一个合理值,让请求平稳地流过去,总吞吐量反而更高。

4.3 批量请求的编排与结果收集

异步请求最典型的用法就是批量处理。假设你有 50 条文本需要分类,可以这样编排:

async def batch_process(texts): tasks = [limited_request(f"分类这条文本:{t}") for t in texts] results = await asyncio.gather(*tasks, return_exceptions=True) final = [] for i, r in enumerate(results): if isinstance(r, Exception): print(f"第 {i} 条处理失败:{r}") final.append(None) else: final.append(r.choices[0].message.content) return final

这里用 gather 来并发执行所有任务,return_exceptions=True 让单个任务的异常不会导致整个批次失败。结果收集的时候逐条判断是正常结果还是异常,异常的就记 None,后续可以单独重试。这种模式在处理大批量数据时非常实用,不会因为一条失败就全军覆没。

提示:asyncio.gather 默认会等待所有任务完成,如果你希望某个任务失败后立即取消其他任务,可以用 asyncio.wait 配合 FIRST_EXCEPTION 模式。但大多数批量处理场景下,我们更希望“能跑多少跑多少”,所以 gather 加 return_exceptions 是更常见的选择。

4.4 异步请求的常见问题与排查

异步请求最常见的问题是“事件循环被阻塞”。什么意思?就是你在异步函数里调用了同步的阻塞代码,比如 time.sleep()、同步的数据库查询、同步的文件读写。这些操作会把整个事件循环卡住,导致其他协程全部无法执行。解决办法是用 asyncio.sleep() 替代 time.sleep(),用异步数据库驱动替代同步驱动,用 aiofiles 替代普通文件操作。

第二个问题是“协程泄漏”。你创建了协程但没有 await 它,或者创建了任务但没有保存引用,导致协程在后台悄悄运行然后报错,而你完全不知道。排查方法是开启 asyncio 的调试模式,它会在协程被垃圾回收时打印警告。具体做法是在程序入口加一行asyncio.get_event_loop().set_debug(True)。

第三个问题是“连接池耗尽”。当并发量超过连接池大小时,新的请求会等待可用连接。如果等待时间超过了超时设置,就会抛异常。排查方法是监控连接池的使用率,如果长期处于满载状态,就需要调大连接池或者降低并发量。我一般会在日志里记录每次请求的等待时间,如果发现等待时间明显增长,就是连接池不够用的信号。

5. 流式请求:用户体验优先的交互方式

5.1 流式请求的工作机制

流式请求和前面两种的本质区别在于响应的接收方式。同步和异步都是“发一次请求,等一个完整响应”,流式是“发一次请求,持续接收数据块”。服务端每生成一个 token 或者一小段文本,就立即推送给客户端,客户端收到后可以立刻展示给用户。

这种机制在交互式场景下体验极好。用户问一个问题,模型开始回答,文字一个个蹦出来,用户感觉系统反应很快。如果等完整响应再展示,用户可能要盯着 loading 转圈好几秒,体验差很多。所以但凡是对用户体验有要求的场景,流式几乎是必选项。

但流式也有代价。首先,你不能像同步请求那样直接拿到完整的 JSON 做解析,得自己处理数据块的拼接。其次,流式请求的错误处理更复杂,因为可能在传输中途断开。再次,流式请求不太适合批量处理,因为它的优势在于“边生成边展示”,批量场景下没有展示需求,用流式反而增加了处理复杂度。

5.2 流式数据的接收与拼接实现

流式请求的响应通常是一系列以特定格式分隔的数据块。以常见的 SSE 格式为例,每个数据块长这样:

data: {"choices":[{"delta":{"content":"你"}}]} data: {"choices":[{"delta":{"content":"好"}}]} data: [DONE]

你需要逐行读取,遇到data:开头就解析 JSON,取出 delta 里的 content 拼接到结果字符串上,遇到[DONE]就结束。下面是一个简化的实现:

import requests import json def stream_request(prompt): url = "https://api.example.com/v1/chat/completions" headers = { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY" } payload = { "model": "某模型名称", "messages": [{"role": "user", "content": prompt}], "stream": True, "temperature": 0.7, "max_tokens": 512 } full_content = "" with requests.post(url, headers=headers, json=payload, stream=True, timeout=60) as resp: for line in resp.iter_lines(): if not line: continue line_str = line.decode("utf-8") if line_str.startswith("data: "): data_str = line_str[6:] if data_str.strip() == "[DONE]": break try: chunk = json.loads(data_str) delta = chunk["choices"][0]["delta"] if "content" in delta: content_piece = delta["content"] full_content += content_piece print(content_piece, end="", flush=True) except (json.JSONDecodeError, KeyError, IndexError): continue return full_content

这段代码的关键点在于:stream=True 告诉 requests 不要一次性读取全部响应,而是以流的方式逐行读取。iter_lines 会按行迭代,每次拿到一行数据。解析的时候要处理各种异常情况,因为流式数据块偶尔会出现格式不完整的情况,比如网络抖动导致一行被截断。用 try-except 包住解析逻辑,遇到坏数据就跳过,不要让它中断整个流。

5.3 流式场景下的超时与中断处理

流式请求的超时设置和同步请求不一样。同步请求的超时是“从发出请求到收到完整响应”的总时间,流式请求的超时通常分为两部分:连接超时和读取超时。连接超时是指建立连接的时间,读取超时是指两次数据块之间的最大间隔。

我一般把连接超时设 10 秒,读取超时设 30 秒。读取超时不能设太短,因为模型在生成某些复杂内容时,两个 token 之间的间隔可能会比较长。如果设太短,正常生成过程中就被断开了,用户看到一半内容没了,体验很差。

中断处理也是流式场景必须考虑的。用户可能在生成过程中点击“停止”按钮,这时候你需要主动关闭连接。在 Python 里,可以用 resp.close() 来关闭流式响应。关闭之后,已经接收到的内容要保留,不能丢掉。我见过一些实现,用户点停止之后已经生成的内容也消失了,这就是没有做好中断时的状态保存。

还有一个坑是流式请求的重试。同步请求重试很简单,重新发一次就行。流式请求如果中途断了,重试的话是从头开始还是从断点继续?大多数模型服务不支持断点续传,所以只能从头开始。但从头开始意味着用户已经看到的内容会重复,体验不好。我的做法是:如果流式请求在生成早期就断了(比如前 10 个 token 内),直接重试;如果已经生成了一大半才断,就不重试了,把已生成的内容展示给用户,并提示“生成中断”。

5.4 流式与非流式的性能对比实测

我做过一组实测,对比流式和非流式在相同任务下的表现。任务是用模型生成一段 300 字左右的文案,分别用流式和非流式各跑 20 次,记录首字节时间和总完成时间。

指标非流式流式
首字节时间2.8 秒0.6 秒
总完成时间3.2 秒3.4 秒
用户感知等待3.2 秒0.6 秒
网络传输量略少略多

从数据可以看出,流式的总完成时间其实比非流式略长,因为数据分块传输有额外的协议开销。但首字节时间从 2.8 秒降到了 0.6 秒,用户感知的等待时间大幅缩短。这就是流式的核心价值:不是让生成更快,而是让用户感觉更快。

注意:流式请求的 max_tokens 设置要比非流式更谨慎。因为流式是边生成边展示,如果 max_tokens 设得太大,用户可能要等很久才能看到完整内容。我一般会根据预期输出长度来设,留 20% 余量就够了,不要像非流式那样留 50% 余量。

6. 三种方式的选型决策与混合使用策略

6.1 选型决策表与判断逻辑

到底该用哪种方式?我整理了一个决策表,你可以对照自己的场景来选:

场景特征推荐方式理由
脚本任务、离线批处理原生 HTTP 同步简单可控,不依赖 SDK
高并发批量处理SDK 异步吞吐量高,SDK 封装完善
交互式对话、实时展示流式首字节时间短,用户体验好
跨语言调用原生 HTTP 同步不受 SDK 语言限制
需要精细控制请求细节原生 HTTP完全透明,无框架黑盒
快速原型开发SDK 异步代码量少,开发效率高

判断逻辑其实就三条:第一,有没有实时展示需求?有就选流式。第二,需不需要高并发?需要就选异步。第三,对第三方依赖敏不敏感?敏感就选原生 HTTP。三条都不满足,那就随便选,哪个顺手用哪个。

6.2 混合使用的最佳实践

实际项目中,这三种方式往往不是互斥的,而是混合使用的。我拿一个典型的 Agent 系统举例:用户在前端输入问题,后端需要先做意图识别(单次请求,用同步或异步都行),然后根据意图决定是直接回答还是调用工具。如果是直接回答,用流式返回给用户;如果是调用工具,工具调用本身可能是异步批量请求,等工具结果回来后再用流式生成最终回答。

这种混合模式下,关键是要统一请求层的接口。我的做法是定义一个统一的请求函数,内部根据参数决定走同步、异步还是流式,对外暴露一致的调用方式。这样上层业务逻辑不需要关心底层用的是哪种方式,切换起来也方便。

还有一个实践是“降级策略”。比如正常情况下用流式,但如果流式连接建立失败,自动降级到非流式同步请求。虽然用户体验差一点,但至少能拿到结果。这种降级逻辑在线上环境非常有必要,因为流式请求对网络稳定性的要求比非流式更高。

6.3 成本与性能的平衡计算

选型的时候不能只看技术,还得算成本。三种方式在 token 消耗上其实是一样的,因为计费是按输入输出 token 数来的,和传输方式无关。但在重试场景下,流式的成本可能更高,因为流式中断后重试是从头开始,已经生成的 token 白费了。非流式中断后重试,之前那次请求可能根本没产生输出,不消耗输出 token。

我算过一笔账:假设流式中断率是 5%,每次中断平均浪费 200 个输出 token,一天 10000 次请求,那每天浪费的 token 是 10000 乘以 5% 乘以 200 等于 100000 个 token。按某模型的价格算,一天多花几块钱,一个月多花一百多。不算多,但如果你的中断率更高或者请求量更大,这个数字就会很可观。

所以我的建议是:流式请求一定要做好中断检测和快速失败。如果连接建立后 5 秒内没有收到任何数据块,直接断开重试,不要傻等。这样能把浪费的 token 降到最低。

7. 常见问题排查与避坑经验实录

7.1 请求超时与连接失败的排查思路

超时和连接失败是最常见的问题,排查思路可以按这个顺序来:先确认网络连通性,用 curl 或者 telnet 测试接口地址是否可达;再确认 API Key 是否有效,有时候 Key 过期了返回的错误信息不明显;然后检查请求体格式,特别是 JSON 的字段名和数据类型;最后看服务端状态,如果服务端在维护或者过载,你这边怎么调都没用。

我遇到过一个很隐蔽的问题:请求偶尔超时,但大部分时候正常。排查了半天发现是 DNS 解析不稳定,有时候解析到了慢的节点。解决办法是在 HTTP 客户端里配置 DNS 缓存,或者直接用 IP 地址加 Host 头的方式绕过 DNS。这个问题在容器化环境里特别常见,因为容器的 DNS 配置有时候不太靠谱。

7.2 返回结果异常的调试方法

返回结果异常包括:返回空内容、返回乱码、返回格式不符合预期、返回内容被截断。空内容通常是 max_tokens 设太小,模型还没来得及输出就达到了上限。乱码一般是编码问题,确认请求和响应都用 UTF-8。格式不符合预期可能是 prompt 约束不够强,或者 temperature 太高导致模型自由发挥。截断就是 max_tokens 不够,加大就行。

调试的时候我习惯把完整的请求体和响应体都打到日志里,但要注意脱敏,别把 API Key 和用户隐私数据打进去。日志级别用 DEBUG,只在排查问题时开启,平时关掉避免日志膨胀。

7.3 速率限制与重试策略的实战配置

速率限制是模型服务商保护自己系统的机制,你没法绕过,只能适应。常见的限制维度有:每分钟请求数、每分钟 token 数、并发连接数。应对策略就是指数退避加队列。

我的标准配置是:最大重试 3 次,第一次等待 1 秒,第二次 2 秒,第三次 4 秒。如果三次都失败,就把请求放入延迟队列,过 30 秒再试。延迟队列里的请求不再重试,只尝试一次,失败就记录到死信队列,人工介入处理。

提示:不要用固定间隔重试,比如每次都等 1 秒。因为如果服务端是因为过载才限流,你固定间隔重试只会加重它的负担。指数退避给服务端喘息的时间,成功率更高。

7.4 常见问题速查表

问题现象可能原因排查方法解决方案
请求超时网络不通、服务端过载、超时设太短用 curl 测试连通性,查看服务端状态页调整超时时间,增加重试
返回 401API Key 无效或过期检查 Key 是否正确、是否过期更换有效 Key
返回 429触发速率限制查看响应头中的限制信息降低并发,增加退避等待
返回空内容max_tokens 太小、prompt 有问题检查 max_tokens 设置,简化 prompt增大 max_tokens,优化 prompt
流式中断网络抖动、服务端主动断开查看中断时的错误信息实现断线重连或降级到非流式
响应乱码编码不一致检查请求和响应的 Content-Type统一使用 UTF-8
连接池耗尽并发量超过连接池大小监控连接池使用率调大连接池或降低并发

这张表里的每一行都是我实际踩过的坑。特别是流式中断那个,我一开始没做降级处理,结果网络一抖动用户就看到半截内容,投诉了好几次。后来加了降级逻辑,流式失败自动切非流式,虽然慢一点但至少完整。

7.5 我个人的避坑清单

最后分享几个我总结的避坑要点,都是血泪教训:

第一,永远不要相信网络是稳定的。任何一次请求都可能失败,所以重试逻辑是必须的,不是可选的。

第二,永远不要假设响应格式是固定的。服务端可能升级、可能出 bug、可能返回意料之外的字段。防御性解析能帮你省掉很多深夜修 bug 的时间。

第三,永远不要在循环里发同步请求而不加延迟。你以为你在快速处理,实际上你在快速触发速率限制。

第四,流式请求一定要设读取超时。我见过有人没设读取超时,结果服务端挂了但连接没断,客户端傻等了一个小时。

第五,日志要打全但要注意脱敏。请求 ID、耗时、状态码、token 用量这些都要记,但 API Key、用户输入内容这些敏感信息不要记。

第六,上线前一定要做压力测试。你以为你的代码能扛住 100 并发,实际可能 10 并发就崩了。提前测出来,比线上出问题强。

这些经验看起来简单,但每一条背后都有真实的事故。希望你看完之后能少走点弯路。单次模型请求虽然只是 Agent 系统里的一小环,但这一环的稳定性直接决定了整个系统的可用性。把这一环做扎实了,上面的多轮对话、工具调用、记忆管理才有意义。

相关新闻

C#纯.NET工控仪表盘控件库:圆表直表旋钮开关指示灯全包

C#纯.NET工控仪表盘控件库:圆表直表旋钮开关指示灯全包

简介:这份C#仪表控件示例面向工控、组态及仿真界面开发者,尤其适合需要在WinForm应用中快速搭建可视化仪表界面的初中级程序员。资源以ActiveX控件技术为核心,演示了圆表、直表(测量标尺)、旋钮、开关和指示灯等常见仪…

2026/10/20 20:32:50 阅读更多 →
PyTorch转ONNX再到MNN:端侧图像模型部署全流程与踩坑实录

PyTorch转ONNX再到MNN:端侧图像模型部署全流程与踩坑实录

前阵子帮人将一个图像分类模型塞进移动端跑,模型在服务器上精度明明没问题,一换推理框架就开始出各种幺蛾子。Pytorch模型转成ONNX和MNN这件事,听起来就是两行命令的事,实际推进时要把图的导出机制、算子兼容、精度对齐全走一遍&a…

2026/10/20 20:32:50 阅读更多 →
Unity Lua开发环境搭建:EmmyLua配置、热重载与调试实战

Unity Lua开发环境搭建:EmmyLua配置、热重载与调试实战

2. 为什么我在Unity里选了Lua,以及EmmyLua到底解决了什么先说结论:如果你的Unity项目已经决定用Lua,或者正在纠结要不要上Lua,那“Unity Lua EmmyLua”这套组合,基本是目前我实测下来最舒服的写码方式。这篇文章不搞…

2026/10/20 20:32:50 阅读更多 →

最新新闻

代码托管平台访问慢?从DNS到资源加载的全链路优化指南

代码托管平台访问慢?从DNS到资源加载的全链路优化指南

1. 为什么打开代码托管平台会慢:先搞懂瓶颈在哪很多人一遇到页面转圈,第一反应就是“网不行”,然后开始重启路由器、切换网络,折腾半天发现该慢还是慢。我刚开始也这样,后来才意识到,打开这类海外代码托管平…

2026/10/21 5:28:10 阅读更多 →
PHP性能调优实战:Xdebug与QCacheGrind定位慢接口分析

PHP性能调优实战:Xdebug与QCacheGrind定位慢接口分析

做PHP开发这几年,我最大的体会是:性能问题不是靠猜,而是要靠数据说话。一个接口从20ms变成200ms,一行SQL的事;一个页面响应慢到让人抓狂,可能只是循环里不小心嵌套了一次查询。但难的不是修,而是…

2026/10/21 5:28:10 阅读更多 →
Agent零基础实操指南:用低代码搭一个能干的数字分身

Agent零基础实操指南:用低代码搭一个能干的数字分身

1. 为什么现在都在聊Agent?先搞清楚它到底解决了什么问题我接触过不少刚工作一两年的职场新人,也包括还在上学但已经开始实习的同学,大家挂在嘴边的一句话是:“事情太多太杂,精力不够用。”写周报、整理会议纪要、找文…

2026/10/21 5:28:10 阅读更多 →
ARM Linux嵌入式libiconv构建与字符集转换实战指南

ARM Linux嵌入式libiconv构建与字符集转换实战指南

简介:本资源是专为ARM架构Linux嵌入式系统定制的libiconv字符编码转换库,面向嵌入式开发工程师、交叉编译实践者及需在树莓派、ARM网关等设备上处理多语言文本(如UTF-8/GB2312/ASCII互转)的技术人员。资源通过arm-none-linux-gnue…

2026/10/21 5:28:10 阅读更多 →
降AI率有套路!3步把论文AIGC检测率从80%降到5%

降AI率有套路!3步把论文AIGC检测率从80%降到5%

又到交论文的季节了,后台收到好多私信,问的最多的就是“AI率太高怎么办”。说实话,我看到不少同学用AI辅助写完后,一检测,AI率直奔80%。这个数字确实吓人,但别慌,这题我能解。这篇就把我自己实操…

2026/10/21 5:28:10 阅读更多 →
在线字典学习MATLAB实现:稀疏编码与字典更新实战指南

在线字典学习MATLAB实现:稀疏编码与字典更新实战指南

简介:在线字典学习代码是一套基于MATLAB的机器学习实现资源,面向处理文本、音频等序列数据的开发者与研究者。代码演示了在线学习与字典学习的结合:通过逐样本或小批量方式更新模型参数,在节省内存的同时适应数据流场景&#xff0…

2026/10/21 5:27:10 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起:用户到底在追求什么“封号重开”这四个字,放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流,背后是大量的时间和精力成本。正常情况下,没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”,而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时,就意识到:单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →