资讯详情

从 API 报错到终端光标:我用 AI 写了个 AI Agent 的排错全记录

📅 2026/10/21 17:18:49 | 华诺云谱 👁 阅读
从 API 报错到终端光标:我用 AI 写了个 AI Agent 的排错全记录
项目快收尾的那个晚上我在终端里盯着一个怎么也刷新不掉的光标屏幕上残留着七八行互相叠印的日志进度条每隔几百毫秒就把整片区域重画一次。这种感觉很难形容明明主流程已经通了API 能收了工具也能调了偏偏被一个不起眼的终端渲染问题拖住而且这个 bug 大概率就是我自己让 AI 写出来的。这一轮下来我的总结其实就一句话让 AI 帮我写一个 AI Agent难的不是让大模型生成代码而是要亲手把 AI 生成代码里那些想当然的部分全部揪出来擦干净。整个项目从 API 报错、环境变量加载顺序、重试风暴一路排查到终端控制序列最后反而提炼出一套我自己称之为感应模式的设计思路。这篇文章就把这整条链路完整记录下来适合两类人看一类是正在用 AI 辅助写工具但又经常被边角 bug 卡住的开发者另一类是对 Agent 架构和调用策略感兴趣、想做一个低延迟低成本终端的个人助手的朋友。1. 把AI 写 Agent从想法拆成可运行的工程骨架先说清楚我要做的东西到底是什么。它不是一个聊天机器人而是一个常驻终端里的个人任务 Agent平时挂在一个目录里能用自然语言下发任务它能自己拆解步骤、调用本机命令行工具、读取文件、汇总结果并输出一段简短的结论。简单说就是一个住在终端里的实习生。既然标题说我用 AI 写了个 AI Agent那第一步其实是和 AI 一起做需求拆分。很多人让 AI 写东西失败不是模型不行是目标太大。我一开始也犯过这个错误丢给它一句帮我写一个全能 Agent它会还给你一份八百行的大杂烩看起来什么都能干跑起来什么都不能用。后来我换了一种合作方式把项目拆成三个独立的里程碑每个里程碑都能独立验证。第一个里程碑是打通模型调用链路。Agent 不需要一上来就懂怎么操作系统它只需要做到给我一句任务描述返回一个结构化的执行计划。这一步验收标准很简单命令行传参能收到正常的模型回复并且整个请求链路有日志。第二个里程碑是工具调用闭环。Agent 需要能识别任务里的动作比如查一下当前目录下最大的三个文件它要能自己调用 shell 命令、拿到输出、再把结果塞回上下文继续推理。这个阶段需要设计工具注册表、工具结果回填、还有错误传递。第三个里程碑才是交互体验。包括流式输出、进度提示、终端渲染、退出恢复、并发任务排队。我现在回头看前两个里程碑加起来花的时间大约占总工时一半第三个里程碑占据了剩下的一半而且这一半里至少三分之一都耗在终端渲染这个细枝末节上。这种拆法本质上是把一个锦上添花型项目变成风险可隔离型项目。每个里程碑都有明确的失败边界第一个里程碑失败了说明 API 集成有问题第二个失败了说明工具设计有问题第三个失败了只影响体验不影响核心能力。这样排查问题时不会出现改 A 坏 B的连锁噩梦。技术选型上我特意没有让 AI 自由发挥。我明确了三个约束运行时用 PythonHTTP 客户端用 httpx所有外部工具的出入参必须是 JSON 可序列化的字符串。这个约束看起来多余但非常救命。因为 Python 做胶水语言生态最全httpx 的异步能力让 Agent 在等待模型返回时可以继续干别的事而强制 JSON 化出入参意味着未来加一个新的命令行工具不需要改 Agent 核心代码只需要在工具注册表里加一条记录。这种结构一旦定下来AI 生成的代码即使有不完美的地方修起来也有边界。2. API 报错不是玄学鉴权、限流、schema 三大类坑的排查记录这个项目的 API 对接环节我一个下午踩了三类报错鉴权失败、限流、请求体结构不匹配。问题本身都简单但每一类的背后其实都有值得沉淀的排查逻辑。2.1 第一次跑通的假成功项目刚开始时我用 AI 写了一个最小的调用脚本模型确实返回了一段文本我一度以为 API 对接已经完成了。但实际上那只是网络通、密钥有效、返回有内容三个条件的叠加离可复用还差得很远。真正暴露问题的是把这个脚本嵌入 Agent 主循环之后。Agent 会在一个任务里调用多次模型每次调用之间还夹着工具执行、日志打印、上下文拼接。当某一次调用突然返回空字符串时整个 Agent 直接崩溃了而且没有任何日志说明是哪一步出错。这里第一个经验是单机调试和接入系统是两个世界。单独跑一个 demo 成功只能证明最小路径通但 Agent 场景下调用频率、上下文长度、并发都会变化任何一环没考虑都可能在联动时爆发。2.2 401 鉴权问题环境变量加载顺序引发的连环坑第一次稳定的报错是 401。关键在于这个报错不是一开始就出现而是偶尔出现。我排查了很久才发现问题出在 AI 生成的环境变量加载方式上。AI 生成了一段类似这样的代码from dotenv import load_dotenv import os load_dotenv() API_KEY os.getenv(API_KEY) BASE_URL os.getenv(BASE_URL)看起来没问题但它生成的另一个工具模块里却是在模块加载时直接读取了密钥# tools/network.py import os KEY os.environ[API_KEY] # 模块顶部直接读取问题出现了如果 tools/network.py 在 load_dotenv() 之前被 import那么 os.environ 里还没有 API_KEY这个工具模块就会带着一个空密钥进入运行状态。更糟的是一旦这个 Key 是 Nonerequests/httpx 在 Authorization 头里会写入 Bearer None服务端返回的 401 根本不是密钥错误而是协议格式无效。这个报错信息极具迷惑性。修复方案不是把 load_dotenv() 放在文件最顶部这么简单而是从根本上改变密钥加载的时机不允许任何模块在 import 阶段读取环境变量统一提供一个 get_secret() 函数只有真正发起请求时才读取。from functools import lru_cache lru_cache(maxsize1) def get_secret(name: str) - str: value os.getenv(name) if not value: raise RuntimeError(fmissing secret: {name}) return value2.3 429 限流重试策略怎么写才不算暴力重试401 解决完成后紧接着是 429。这里有一个很显眼的坑AI 自己写了一段重试 3 次的代码却没有任何退避策略。任务并发一多就会变成对所有请求同时发起重试反而触发更严苛的限流形成恶性循环。正确做法是固定间隔 指数退避 抖动。注意抖动一般人都不会主动加但它很重要如果多个 Agent 任务同时失败并同时开始退避它们会再次在同一个时间点打满配额抖动就是让重试时间点发散避免共振。import random import time import httpx def post_with_retry(client: httpx.AsyncClient, *args, **kwargs): max_attempts 5 base_delay 1.0 for attempt in range(max_attempts): try: response await client.post(*args, **kwargs) if response.status_code in (200, 400, 401, 422): return response if response.status_code 429: delay base_delay * (2 ** attempt) delay random.uniform(0, delay * 0.2) await asyncio.sleep(delay) continue return response except (httpx.ConnectError, httpx.ReadTimeout) as exc: delay base_delay * (2 ** attempt) random.uniform(0, 0.5) await asyncio.sleep(delay) continue return response还有一个被忽略的点读错误码头里的 retry-after而不是自己拍脑袋定退避时间。每个平台返回的 429 里往往带了建议等待时长直接用它做 sleep 最稳。如果服务端没有给这个字段再用上面的指数退避兜底。2.4 400 schema 不匹配把普通 JSON 报错变成可读提示第三类报错是 400。出现场景是AI 写的客户端和当前接口版本不一致请求里某字段要求是 enum我们传了自由字符串或者 messages 列表里应该有 role 字段我们漏传了。这种报错特别容易出现因为 AI 生成代码时会根据训练数据猜接口结构但模型接口只要升级过一次老的数据形态就废了。解决办法是给日志系统加一个明文留痕把每个请求体的 JSON、响应体、状态码、耗时统一写入一个结构化日志文件报错时直接看留痕就能发现 schema 问题完全不用猜。后来我把这个日志格式推广为整个项目的通用约定[2025-06-12 14:02:11] [txn8f3a9d] [POST] [apichat] [status400] [took742ms] request_body: {model:..., messages:[{role:user, content:...}]} error_payload: {error:{param:messages.0.content,type:invalid_string,message:content cannot be empty}}像这样的留痕一旦形成你就是闭着眼也能定位问题。2.5 用日志还原现场给 API 调用加上透明观测整个 API 排错阶段最值钱的动作不是修好某一个 bug而是建了一套足够透明的观测机制。我给每次模型调用都生成了一个 txn_id贯穿整个 Agent 任务周期。任务开始、工具执行、模型调用、结果返回所有日志都会带上这个 txn_id这样即使有 20 个任务并发也能单独拎出一个任务的完整时间线。这套机制把排查时间从小时级降到分钟级。没有任何魔法就是把所有状态暴露出来每个请求有没有带上正确的密钥、是触发了限流还是被拒了、是服务端超时还是客户端超时、token 消耗是多少。如果没有这套观测后面终端光标 bug 会更难找因为你会怀疑是不是日志框架坏了——但其实是控制序列互相污染。3. 终端光标 bug一个看似不起眼却藏得很深的交互缺陷项目核心功能全部跑通后我进入体验打磨阶段。然后一个非常诡异的问题出现了Agent 在执行长任务时终端会呈现一种失控状态。多行日志会互相覆盖进度栏会闪回命令行输入框甚至会消失一小段时间。乍一看像终端卡了但任务本身还在正常推进。3.1 现象描述光标乱跳、进度条错位、多行日志重叠我把复现过程描述得更精确一些Agent 启动后终端底部出现一个动态进度条上面显示正在执行第 3/7 步...。与此同时工具日志还在一行一行往外打。正常情况下进度条应该固定在最底部日志从下方往上推但实际看到的是进度条偶尔跳到中间位置然后日志直接从进度条上方覆盖过去留下一堆残缺字符。另一个很典型的症状是任务结束后的提示符并不出现在新一行而是紧跟在最后一段输出的末尾就好像终端忘了补一个换行。这个现象看起来像是某种终端渲染状态错乱。3.2 排查链路从怀疑日志库到修复 ANSI 控制序列我一开始完全没有往字符流方向去想因为这种 bug 太不像业务问题了。我第一反应是怀疑自己用的第三方日志库在动态渲染场景下有 bug。为了验证我写了一个最小复现脚本只创建一个子进程让它每秒输出 3 行普通日志主线用另一个任务输出进度条。跑完一看问题依旧说明跟第三方库无关问题出在我自己的输出逻辑里。然后我开始逐个环节做二分定位关掉进度条日志正常保留进度条、关掉子进程输出进度条正常两者同时开立刻错乱。我把终端输出用 repr() 抓下来看才看到了本质print(repr(progress_cache))输出里出现了一堆 \r回车、\n换行、\x1b[2K清除整行、\x1b[G光标移动到行首这些控制序列。问题原理其实很简单终端的光标位置完全由收到的字节流决定。当子进程输出多行日志时它会给日志行结尾加上 \n这个换行会把光标推到下一行但动态进度条为了让进度条始终在底部用了 \r 回到本行行首再重画内容。两个逻辑同时操作同一个光标位置就互相打架了。3.3 为什么 AI 生成代码容易在这类细节翻车这其实是最值得反思的一层。AI 很擅长生成看起来合理的进度展示代码它知道要用 \r 实现不换行的刷新但它不知道的是进程在同一时刻可能还在向同一标准输出写其他内容。更麻烦的是如果终端窗口被用户手动缩放过那么终端的列宽也会变化。如果窗口变窄之前画的整行内容就只能在下一行部分渲染如果窗口变宽\x1b[2K 清理的可能不是完整的一行如果没有捕获 SIGWINCH 信号进度条计算出来的宽度就是旧宽度。这些边界条件普通人在手写代码时往往也不会想到AI 更不会主动生成。而这些东西恰恰是终端程序事故率最高的地方。如果 AI 生成了永远假定 80 列终端、永远假定自己独占输出的代码那它在真实环境里运行必然翻车。3.4 修复方案与回归验证最终的修复不是删掉进度条而是做一个统一的终端渲染器。所有输出无论是工具日志还是进度条都必须经过同一个方法出口。动态渲染类的输出要注册到一个 reducer 里同一时间只允许一个动态区域存在静态日志在进入动态区域之前先把动态区域清掉等日志输出完再重绘进度条。我加的工具里最关键的是捕获 SIGWINCH 信号窗口尺寸一变所有动态区域重算宽度并立即重绘。进度条每 100ms 刷新一次但检测到窗口变化时会强制插入一次刷新。修复之后的回归验证我用了一个比较笨但管用的方法把终端窗口从 120 列拖到 40 列再拖到 180 列反复运行 Agent 任务确保输出不会错位。又连续跑了 200 轮自动化任务确认没有一行日志被覆盖。终端渲染这个 bug 表面上很小但它直接告诉你一个道理Agent 本质上是长驻进程任何交互模块都必须考虑到 24 小时挂在终端里不退出时的状态持久性。4. 从 bug 到感应模式一次规格进化记录如果说前面都是纯排错那这个项目的第四部分是真正的设计产出。修完终端光标 bug 之后我有一个观察Agent 大部分时间做的事情其实非常琐碎很多请求根本不需要大模型深度推理。比如用户输入列出当前目录文件即使这个句子没有触发正则我们也完全可以直接抛出 os.listdir 的结果不必经过一次完整模型调用。这个观察最终被我总结成一个设计模式我内部叫它感应模式。取名字的灵感来自于传感器Agent 在任何任务开始之前先经过一层轻量级的感应层用来判断当前请求需不需要唤醒大模型、需要多大能力的模型、要不要带着完整上下文进入、还是说这在本地就能短路掉。4.1 从效率问题提炼出感应模式原来的 Agent 循环是标准的用户输入自然语言 —— 拼接全部上下文 —— 调用模型 —— 获得回复/工具调用 —— 再次调用模型。这个循环最严重的成本问题在于即使是很简单的一句话也会把历史对话、工具结果、系统提示全部拼进去发送到模型接口等待推理然后流式显示。很多任务其实可以被分成三种类型第一类是直接路由型。说白就是用户输入本身就是命令帮我打开项目目录运行测试查看配置。这些内容命中关键词后根本不需要语义理解。直接把命令映射到工具函数立刻返回结果。第二类是上下文查询型。比如上次说的那个问题后来怎么解决的这类请求看起来是语义需求但实际只需要从本地的历史任务索引里查一下。它需要轻量级模型辅助判断但不一定需要启动完整推理链条。第三类才是深度推理型。比如分析这个报错可能是什么原因给出三个排查方向这种必须调用大模型而且需要把完整文件文本、工具输出和提示词全部喂进去。感应层做的事情就是在进入模型调用之前先把输入做一次预分类。预分类我会用相当轻量的手段关键词路由、正则匹配、token 长度预估和一个用简单规则训练的本地分类器。4.2 感应器的设计关键词路由 token 长度预检设计过程中我最担心的是把感应层做成一个自欺欺人的黑盒预分类器判断错了把复杂的语义请求当成简单命令处理了结果用户收到一句冰冷的话不支持请用自然语言描述体验反而更差。所以感应层必须带上置信度和兜底逻辑。一个安全的模式是疑似直接路由但不确信时交给大模型兜底。具体流程我写成代码是这样的SENSING_RULES [ (list_dir, r^ls$|^列出.*(文件|目录)|^show.*files?), (run_test, r^(run|执行)?(测试|test).*$), (open_file, r^(打开|open|查看).*\.(py|js|md|json)$), ] def sensing_route(user_input: str) - RouteDecision: for action, pattern in SENSING_RULES: if re.search(pattern, user_input, re.IGNORECASE): return RouteDecision(actionaction, confidence0.9) else: return RouteDecision(actionllm_full, confidence0.0)token 长度预检是另一个有意思的设计。输入原始文本之后就先用一个非常高效的 token 预估器估算 token 数如果一整个任务都塞进上下文会触发上下文超长就把任务拆成多个子任务每个子任务只携带自己需要的上下文进入大模型而不是像原来那样把全部历史一股脑发给模型。4.3 感应模式不是简单规则引擎有人可能会说这不就是个 rules engine 吗我跟朋友解释的时候打过比方更像是 CPU 里的缓存系统如果数据正好在 L1 缓存里直接命中就返回如果没命中才去主存里取。关键区别在于CPU 缓存靠的是地址映射和局部性原则感应器靠的是任务重复度和语义可压缩性。在 Agent 场景里很多输入天然带有结构。比如把上一个任务的输出保存到 notes.txt这句里上一个任务这个概念就可以靠本地状态索引完成根本不需要把上一个任务的完整上下文塞进模型。感应层在这里不是简单匹配保存文件这些词而是要维护一个任务上下文栈当一个请求引用了上一个上次这类指代词感应层要从任务栈里把对应快照拿出来而不是凭空推理。4.4 实测数据延迟、成本和体感的变化搭完感应层之后的对比非常明显。我拿一套 50 条混合任务命令、查询、深度分析各占一部分做测试显示效果是这样的指标全量对话模式感应模式平均单轮响应延迟4.8 秒1.2 秒Token 消耗总量50 条任务约 18 万 token约 6.7 万 token需要等待思考中的轮次占比100%约 22%整体任务完成率96%100%这个数据不严谨因为样本量很小而且任务构成不止公平占比。但趋势非常明显至少三分之一的任务是纯命令类型根本不需要深度推理还有一部分任务可以直接在本地索引里查答案而不用发起网络请求。成本降低也不是最重要的。体感改善才是最值得注意的用户从每一次输入都要等好几秒变成大多数输入秒回这种感觉上的差异会让 Agent 从一个玩具变成真正每天在用的工具。4.5 边界条件哪些场景必须强制关闭感应模式感应模式虽然好用但必须明确何时不能感应。我把这几条直接写进了设计文档复杂问题不允许预分类。当用户输入包含多个意图时比如同时告诉我最大的文件、运行一下测试、然后把结果写进 report.md本地感应器很难可靠拆分这时候必须全量走大模型。情感/模糊表述不允许预分类。比如用户说最近怎么这么慢你该优化一下了吧这种话不经过完整语义理解是看不出它到底求字面反馈还是请求自动优化。规则引擎大概率会匹配到优化两个字触发一个跑优化流程的工具——这绝对是大事故。凡是涉及文件删除、重写、覆盖的操作感应器永远只能提示疑似不能直接执行必须通过大模型的完整判断加上用户二次确认。这些边界条件看起来好像是在为感应模式打补丁但实际是一个 Agent 真正可靠的关键。一个好用的机制必须带着清晰的失效兜底你可以省去 80% 的模型请求但对剩下 20% 的请求要百分之百转到完整通道。省出来的效率只有在不降低可靠性时才叫优化。最后分享一点个人体会。整个项目做下来我最深的感受是AI 帮我写 Agent真正的价值不是它写了一切而是它让我能以极低的成本把脑子里的想法变成一个可以运行、可以调试、可以迭代的骨架。那些 API 报错、环境变量顺序、终端光标错位没有任何一个需要深度学习知识但每一个都需要人来做决策这个报错能接受吗、那条路径要不要留、这个边界条件谁会触发。还有一个很实用的小技巧无论让 AI 生成什么模块都先给它一个日志格式要求而且要求所有被调用的工具函数都返回一个 dict包含 status、raw、summary 三个键。这样一来Agent 的排错体验会清爽得多。任何一步出问题你都能直接从 raw 里看到原始结果从 summary 里看到提炼结论从 status 里知道该不该重试。这个习惯看着小却让整个项目的可维护性上了一个大台阶。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑