最近两周,我把晚上的时间基本都投在了一个有点“自找麻烦”的项目上:用 AI 辅助编码,写一个跑在终端里的 AI Agent。目标很朴素——在命令行里用自然语言下达指令,让它自动拆解任务、调用外部 API、再把结果整理好回显给我。听起来像是个大号聊天机器人套壳,但真正落地远比想象中曲折。我先后撞上了 API 报错、终端光标乱跳、多线程输出互相打架这些坑,最后在一个深夜调试光标 bug 的间隙,反而想明白了一套我自己称为“感应模式理论”的交互设计框架。这篇就来完整复盘这次实战:怎么和 AI 协作写代码,API 报错怎么一步步排查,终端光标 bug 的根因是什么,以及最终沉淀出的“感应模式理论”到底在说什么。
1. 项目思路:先想清楚要做个什么 Agent
1.1 需求拆解:不做一个空壳 Demo
动手之前,我先花了一个晚上把需求写清楚。这个 Agent 不是为了炫技,是要真的在日常工作里用起来。我给自己列了这么几个硬性场景:
- 在终端里输入一句自然语言指令,比如“查一下北京的天气然后帮我整理成一个备忘录”。
- Agent 能识别意图、把任务拆解成步骤,调用对应工具去执行。
- 支持连续会话,能记住上下文,而不是每次对话都“失忆”。
- 所有输出走统一的终端渲染,不能满屏乱打日志。
- 对 API 报错有明确的处理逻辑,快速失败而不是无限重试。
你可能会说,这就是个 RAG 或者简单的 intent-slot 系统,没必要搞这么大动静。但我的判断是:如果不把需求边界划清楚,AI 写代码的时候就会天马行空。给它一个“做一个智能助手”这种模糊指令,它大概率会给你生成一个庞大的 Web 应用,而不是一个你需要的东西。我做的是先压缩范围,明确说不要数据库、不要 Web 框架、不要用户系统,只要一个能在终端里完成上述场景的最小闭环。
这个“最小闭环”思维很重要。AI 辅助编码最容易失控的地方就是范围扩散。每次我说“顺便再支持一下这个功能”,AI 就会给我塞进一堆没验证过的依赖。所以我在项目初期就定了个规矩:凡是当前版本用不到的模块,一律不写;凡是需要额外安装重量级框架的,一律拒绝。
1.2 技术选型:为什么是终端 CLI 而不是 Web 界面
很多朋友问我,为什么不做成网页或者桌面应用?我的理由比较实在:这个 Agent 最核心的使用场景是我的命令行工作流。我已经习惯了在终端里处理文件、跑脚本、查日志,如果它是个网页服务,我还得先打开浏览器、找到书签、再点进去,整个过程太重了。
终端 CLI 的好处是:天然支持管道、重定向、脚本化调用。比如我可以让 Agent 生成一段命令,再直接通过管道接到 shell 里去执行,这是 Web 界面很难做到的。另外一个私心是,终端 UI 的复杂度可控。我现在是在用 AI 辅助编码,如果选型太复杂,后面调试的每一分钟都是在给项目增加风险。
技术栈我最后定成了 Python,搭配几个轻量库:
- 命令行解析用 Typer,生成帮助信息方便。
- 输出渲染全部交给 rich,不手工拼 ANSI 转义序列(这一点后面差点害死我)。
- 网络请求统一走 requests,所有外部 API 都封装成独立工具函数。
- 交互式输入用 rich 提供的 Prompt,而不是裸的 input()。
不用什么 LangChain 之类的重量级框架。原因很简单:项目的核心逻辑是“几个工具 + 一个决策循环”,自己写比引入一个抽象层更可控。而且真出了 bug,我也更容易定位问题。
1.3 整体架构:最小可行 Agent 骨架
最终敲定的架构,我自己起了个名字叫“五层小骨架”,每层职责单一:
- 入口层:解析 CLI 参数、加载配置,比如调试模式开关。
- 会话层:维护多轮对话的上下文,把历史消息压缩成模型能用的摘要。
- Agent 核心:负责意图识别、任务规划、调用工具、汇总结果。这是最薄但最关键的一层。
- 工具层:封装各个外部 API,统一错误处理、超时、重试策略。
- 渲染层:所有输出统一走 rich 的 Console,禁止散落的 print。
这个分层我现在回头看,最大的价值不是“架构漂亮”,而是给了 AI 编码时非常清晰的边界。我对 AI 说“你在工具层加一个函数”,它不需要理解 Agent 核心的意图识别逻辑;我对 AI 说“你在渲染层修一个 bug”,它也不会跑去改请求逻辑。模块独立以后,AI 生成代码的准确率明显高了一截——因为它的注意力范围变小了。
2. 和 AI 结对写代码:协作方式比代码本身更重要
2.1 我不是让 AI “一把梭”,而是拆成小任务
和 AI 写代码,我最大的心得是:别让它“一把梭”。如果你把整个项目的需求一次性丢过去,它给出的答案表面看着完整,实际运行起来全是坑,而且你很难定位是哪块出了问题。
我的做法是把需求拆成一个个小任务,每个任务对应一个函数或一个模块,然后给 AI 下“可验收”的指令。拿工具层的天气查询函数举例,我当时的提示词大概是这样的:
角色:你是一名 Python 开发工程师,熟悉 CLI 工具开发。 任务:实现一个函数 get_weather(city: str), 输入城市名,返回天气信息。 要求: 1. 使用 requests 调用天气 API,拼接 URL 时城市名必须 URL 编码。 2. API key 从环境变量 WEATHER_API_KEY 读取,禁止硬编码。 3. 必须处理以下异常: - requests.Timeout(设置 5 秒超时) - requests.HTTPError(捕获后返回错误信息) - JSON 解析错误 4. 返回统一结构:{"success": bool, "data": dict, "error": str} 5. 同时写一个最小单元测试,覆盖正常返回和超时两种路径。你看,我把函数签名、错误处理要求、返回结构全都定死了。AI 不需要思考“用户想要什么”,只需要填实现细节。这样生成的代码基本不需要大改。关键在于:你把验收标准写清楚,它就不容易自由发挥。
2.2 真实案例:AI 生成的天气查询代码到底长什么样
AI 第一次生成的代码,说实话大框架没问题,requests 调用、URL 拼接、JSON 解析都有,但仔细看有几个细节让我头皮发麻:
第一,API key 直接被拼进了请求参数而不是放到 Header 里。我用的某家天气服务,鉴权要求是放在Authorization头里。AI 之所以这么写,大概率是因为它见过的很多示例代码都是这种写法,但实际服务端不接受,直接 401。
第二,异常处理只包了requests.get本身,但对响应里的业务错误码没处理。比如请求成功了,服务端却返回“城市不存在”的业务错误,AI 的代码照样返回 success=True,这会导致上层 Agent 误以为查询成功。
第三,超时时间没有设置。requests 默认是不超时的,一旦上游服务卡住,整个 Agent 就会挂在那里。
这些问题单看都不大,合起来就是一场事故。我当时没直接让 AI 改,而是把这些问题列成清单,让它逐项修复。这个过程本身也是一种训练——AI 会从你的 review 意见里学到这类项目的约束。
2.3 AI 能干什么、不能干什么,边界画清楚
经过这个项目,我对“AI 辅助编码”的边界有了更清晰的认知:
AI 真正擅长的是:样板代码、类型注解、正则表达式、单元测试、简单数据处理管道、根据清晰的接口文档写客户端。这些活儿规律性强,AI 一学就会。
AI 不擅长的是:业务语义的把控、错误恢复策略的设计、安全边界、性能优化、和你现有代码库的隐式约定保持一致。这些都需要人来盯。
所以我的工作流变成了:AI 写第一版,我 review 并列出问题清单,AI 按清单修,我再测试。这个循环走下来,AI 的价值不在于“一次写对”,而在于“减少我从零敲键盘的时间”。它更像一个效率极高的实习生,而不是一个可以完全托付的资深工程师。你得接受它犯错,然后把纠错的成本控制住。
3. 第一场硬仗:API 报错的完整排查过程
3.1 现象:接口返回 400 和 401
项目第一次跑通端到端流程时,我兴冲冲地输入“查一下上海的天气”,结果控制台抛出一串异常。日志里能看到两个不同的报错:一种是401 Unauthorized,一种是400 Bad Request。
当时第一反应是 API key 过期了。我检查了环境变量,key 明明在。又试了试重启终端,还是不行。这种状态是最烦人的——你有两个不同的错误码,意味着不只是鉴权问题,参数可能也有问题。
好在我在设计工具层的时候,给每个 API 封装都加了统一的日志输出,能看到完整的请求 URL、请求头和响应体。这一步别小瞧,没有它,后面排查至少多花三倍时间。
3.2 排查思路:从表象到根因
我当时拿着日志,一步步往下查:
第一步,确认 API key 本身有没有问题。我把请求头完整打印出来,发现 Header 里根本没有Authorization字段,key 被放到了请求体里。这就是 401 的直接原因。这个锅可以记到 AI 头上——它不知道这家服务商的鉴权契约。
第二步,解决 401 以后,400 还在。我直接用 curl 手动复现了一次请求,竟然成功了。这就很有意思了:同样的参数,curl 能通,Python 请求 400。我对比了一下 AI 生成的代码和 curl 命令,马上发现了问题——AI 把参数名写错了。文档要求的是location,它写成了city。服务端拿到一个不认识但合法的参数,就返回了 400。
第三步,我继续深挖了一下,发现 AI 代码里还有个隐患:没有设置超时。requests 默认永不超时,也就是说一旦上游服务慢,Agent 会一直卡住。这个在本地测试时不一定暴露,但生产环境早晚出事。
3.3 修复手段与经验沉淀
修复本身不复杂,把鉴权头改对、参数名改对、加上超时和重试逻辑就完事了。复杂的是我怎么防止这类问题反复出现。
我总结了“外部 API 客户端检查清单”,后面每加一个新 API 封装,都按这个清单过一遍:
- 鉴权方式确认清楚:Header 还是参数?Bearer 还是 Basic?
- 参数命名以官方文档为准,不靠猜。
- 必须设置超时,通通用 5 秒起步。
- 重试策略要带退避,不要死命重试。
- 响应结构进入系统前要做校验,业务错误码和 HTTP 错误码都要处理。
另外还有一个非常实用的经验:给 AI 下发 API 封装任务时,把官方文档的关键片段直接贴进提示词。别看这个动作小,它能大幅减少 AI 瞎猜参数名的概率。AI 本质上是模式补全,你给它越多的真实上下文,它补得越准。
4. 最难缠的 bug:终端光标乱跳
4.1 复现:退格键和光标像喝醉了一样
API 问题解决以后,Agent 能跑通了,但交互体验还是稀烂。我在终端里输入指令时,只要 Agent 刚打印完一段带颜色的日志,接下来我的输入行就开始“精神错乱”:光标跑到行首,退格键删掉的不是刚敲的字,而是提示符;有时候输出一行没结束,下一行就从中间插进来,文字叠在一起。
这个 bug 最蛋疼的地方是:它不是必现的,只在特定情况下出现,比如任务执行过程中后台线程正好在输出日志,你这边同时开始敲键盘,两个操作一交错,终端就乱了。
我一开始还怀疑是终端模拟器的问题,换了好几个终端测,发现都有类似现象,只是严重程度不同。这才确定问题出在自己的程序里。
4.2 根因分析:多线程打印撞上 ANSI 转义
排查到最后,找到两个根因:
第一个根因,是程序里有多条输出路径。Agent 执行任务的时候,后台线程用print()打印日志,主线程同时用input()等用户输入。两个线程往同一个 stdout 写数据,输出就会交错。终端拿到一串不是按顺序来的字符,自然不知道该怎么显示。
第二个根因更隐蔽:为了实现“好看”的日志,我让 AI 直接拼接 ANSI 颜色码,比如\033[31m 这是一条红色日志 \033[0m。问题在于,如果字符串中某个颜色码没有正确闭合,比如少了个\033[0m,后续所有输出都会带上前一个颜色属性。更严重的是,如果这些控制字符在“输入模式”下被终端误解析,光标的行列计算就全部错乱。
可以这么理解:终端是一个状态机,你对它发送普通字符,它就原样显示;你发送控制字符,它会进入一种“控制模式”。两个线程乱发指令,终端状态机彻底分裂,光标自然乱跳。
4.3 解决办法:统一渲染层
修这个 bug,我做了四件事:
第一,砍掉所有散落的print(),日志全部改用logging模块,默认写入日志文件,控制台只保留关键信息。这样后台线程不会再随便往 stdout 里灌内容。
第二,彩色输出统一交给 rich 库处理,禁止任何人再手工拼接\033[开头的转义序列。rich 内部会正确维护终端的输出状态。
第三,用户输入改成 rich 的 Prompt,不再用裸的input()。这样输入框的渲染和普通输出的渲染走同一个控制逻辑,不会再互相踩。
第四,如果有必须要实时输出的后台进度,放到一个渲染队列里,由专门的输出线程消费,按顺序渲染。
第四点的核心代码我当时大概是这么写的:
import queue import threading from rich.console import Console console = Console() render_queue: queue.Queue = queue.Queue() def render_worker(): while True: msg = render_queue.get() if msg is None: break console.print(msg) def task_worker(): # 业务逻辑 render_queue.put("任务完成") threading.Thread(target=render_worker, daemon=True).start() threading.Thread(target=task_worker, daemon=True).start()只准渲染线程写 stdout,其他线程一律把消息丢到队列里。这个模式很简单,但它能根治“多线程乱写终端”的问题。
4.4 终端交互的几个通用避坑点
- 别自己拼 ANSI 转义,老老实实用现成渲染库。跨平台差异、光标位置计算、颜色闭合处理,库都帮你做了;自己拼只能靠踩坑补齐。
- 别在多线程里直接输出,先确定线程模型:谁负责读输入,谁负责写输出,中间用队列解耦。
- 如果要用全屏交互、键盘事件、补全提示,直接上 prompt_toolkit,别用 input 加控制字符硬造。
- 换终端测试时,注意不同终端对 ANSI 支持有细微差异,统一用库能减少这类问题。
5. 从 Bug 里悟出来的“感应模式理论”
5.1 灵感来源:终端的“感应”给我的启发
修好光标 bug 后的那个晚上,我坐在电脑前看终端里的输出,突然想到了一个问题:终端为什么能给我这么好的交互体验?不管是输入时的光标跟随,还是系统自动补全,本质上都是程序在“感应”我的状态,然后据此调整自己的行为——我把手指放在键盘上,它感应到了按键;我输入到一半,它感应到可能的意图,给出提示。
反观我的 Agent,它的问题是:不管我处于什么状态,它都用同一种方式回应。我指令明确时,它问东问西;API 连续失败时,它还在傻乎乎地重试;上下文信息不够时,它硬着头皮瞎猜。说白了,它没有“感应”能力,像个不看场合说话的人。
我当时就把这套想法整理了一下,给它起名叫“感应模式理论”。
5.2 理论的三个核心层
感应模式理论的核心,是把一个智能交互系统拆成三个层次:
第一层是感知层(Perception)。它负责采集外部信号,包括用户输入、上下文状态、外部 API 的成功率、系统负载等等。没有这一层,系统就是个瞎子。
第二层是模式判定层(Mode Inference)。它把感知到的信号映射成一组离散或连续的模式。比如“当前指令意图置信度高于 0.9”,就可以判定为“执行模式”;“API 连续失败三次”,可以判定为“降级模式”。判定逻辑可以是规则、阈值,也可以是一个小的分类模型。
第三层是行动策略层(Action Policy)。每一种模式都对应一套行动策略,决定系统怎么回应。比如在执行模式下,Agent 应该直接干活少废话;在澄清模式下,Agent 应该先提问题再行动。
这个三层结构放到终端交互里也成立。终端感应到你在输入,就进入“编辑模式”;感应到你在按快捷键,就进入“命令模式”。好的交互系统都暗合这个框架。
5.3 落到 Agent 里的四个实战模式
我把这套理论落到了我的 Agent 里,目前实现了四种模式:
| 模式 | 感知信号 | 判定条件 | 行动策略 |
|---|---|---|---|
| 默认执行模式 | 用户指令、上下文 | 意图置信度高、工具调用链明确 | 安静执行,只输出最终结果 |
| 澄清确认模式 | 意图置信度 | 意图不够明确、多个工具都匹配 | 先提问,确认后再执行 |
| 降级兜底模式 | API 失败率 | 连续失败达到阈值 | 切换本地缓存或离线规则,提示用户 |
| 调试巡检模式 | 异常频率、DEBUG 开关 | 环境变量开启或错误率异常 | 输出详细日志、错误堆栈 |
举两个例子。第一个是“澄清确认模式”。最初 Agent 接到“帮我看看那个文件”这种指令时,不知道“那个文件”指代什么,会直接报错。后来我让它在意图置信度低于 0.6 时,进入澄清模式,反问你“你说的那个文件,是指今天下载的那个,还是昨天改的那个?”这个改动直接把用户体验拉高了一个档次。
第二个是“降级兜底模式”。当时 Agent 依赖的某个天气 API 每到高峰时段就会超时,连续失败三次后,Agent 自动切到本地缓存,把上一次成功查询的结果返回给你,并在后面加一行“注意:这是缓存数据,外部服务当前不稳定”。用户不会觉得整个系统废了,反而会觉得这个 Agent“懂事儿”。
5.4 这个理论不是玄学,是一组工程取舍
有人可能会说,这不就是状态机加策略模式吗?对,你说得没错。但“感应模式理论”真正的价值,是它把“交互系统应该随情境变化而改变行为”这个理念变成了可执行的工程结构。
以前我设计 Agent,脑子里全是“怎么把对话流更完整地抽出来”,结果做出来像个话痨,不管什么情况都说一大串。用了感应模式理论后,我设计的重心变成了“怎么更准确地感知状态、怎么定义更合适的模式、怎么在模式之间平滑切换”。
它也不只适用于 Agent。任何交互式系统都能套用——你有没有发现,老的命令行工具根本不管你心情如何,永远一个态度;而现代工具会感知你的输入场景,给出恰如其分的反馈。这东西确实不是玄学,它就是现代交互系统设计里重要的一环。
6. 常见问题与排障速查表
6.1 API 类问题速查
| 症状 | 可能原因 | 快速排查 | 最终对策 |
|---|---|---|---|
| 401 未授权 | 鉴权头缺失、key 错误 | 打印请求头,对比文档 | 按官方文档设置鉴权字段 |
| 400 请求错误 | 参数名错误、参数类型不对 | 用 curl 复现,对比差异 | 把官方参数样例贴给 AI |
| 请求超时 | 未设置 timeout | 检查 requests 参数 | 统一加 5 秒超时 |
| 偶发失败 | 无重试策略 | 观察错误率 | 加指数退避重试,最多三次 |
| 返回字段错位 | 硬编码解析字段名 | 打印原始响应 | 用模型校验或字典安全读取 |
6.2 终端交互类问题速查
| 症状 | 可能原因 | 快速排查 | 最终对策 |
|---|---|---|---|
| 光标乱跳 | 多线程写 stdout | 检查 print 是否散落 | 统一走渲染队列 |
| 退格键删除提示符 | 输入模式与控制序列冲突 | 换普通终端测试 | 用 rich/prompt_toolkit 接管输入 |
| 输出颜色全部变红 | ANSI 转义未闭合 | 搜索\033[手工拼接 | 改用 rich 等渲染库 |
| 输出内容重叠 | 异步日志插入渲染中 | 查看输出时间戳 | 所有消息进队列,单线程渲染 |
6.3 AI 辅助编码的避坑清单
这段是我这次实战里最想对后来者说的话,全是反直觉但真实的教训。
- 提示词里一定要写错误处理需求。你不写,AI 默认只处理 200 成功路径。把“必须处理超时、HTTPError、JSON 解析错误”写进去,生成的代码立刻靠谱一个档次。
- 生成完代码后,马上用 curl 或 Postman 对照验证。我也说不清为什么,但 AI 生成的请求参数经常和文档有细微出入,对照能快速暴露问题。
- 所有外部依赖都要有超时和重试策略。这是保命条款,不是锦上添花。
- 控制台输出永远统一走渲染层。散落 print 是终端 bug 的第一温床,任何代码审查必须抓住这一点。
- 让 AI 先写测试,再写实现。测试即规格,AI 有了测试驱动,写出来的实现不太会跑偏。
- commit 之前必须自己 review。我试过几次“看起来能跑”就直接提交,后来都付出了代价。别把 AI 当最后的守门员。
结尾
这次实战下来,我对“AI 辅助开发”的看法变了不少。AI 不是神笔马良,它写出来的代码需要你带着审视的目光去读、去测、去改。但它也确实帮我省掉了大量重复敲代码的时间,让我能把精力放在更值得琢磨的问题上——比如“感应模式理论”这种交互设计层面的思考。后面我打算继续把这个理论往下做,试试看能不能用一个轻量分类器来动态推断用户当前的“耐心状态”,让 Agent 自动调整回复的详细程度。不过这些都是后话,至少这次从 API 报错到终端光标 bug 的经历,让我又多了一批可以讲半天的实战故事。如果你也在写类似的 Agent 项目,或者也在用 AI 辅助编码,欢迎到评论区聊聊你的遭遇。