这个项目一句话就能讲完:我用AI编码助手,从零捏出一个能在终端里自主跑任务的AI Agent。任务本身不复杂,就是一个巡检Agent,读取配置、调用服务接口、分析日志,最后把结论生成一份报告。麻烦在于,这个Agent不是一次性脚本,它要一边跑一边向用户实时反馈状态,遇到异常要会重试,还要能把错误处理成可读的结果。
中间踩的坑比代码量还多:API报错、终端光标bug,以及一个我自己总结出来的“感应模式理论”。这篇就是完整的实战记录,从问题现象、排查链路、修复方法,到怎么把经验固化到工程里,都会提到。适合正在用AI辅助写Agent的开发者,也适合所有准备给自己的终端工具加交互反馈的人。
1. 项目怎么定位,以及我为什么敢让AI来写AI
1.1 这个Agent到底要解决什么问题
一开始的需求其实很朴素:我手上有几套内部服务,日志散落在不同机器上,每天要人工巡检一遍状态、检查异常、汇总报告。事情不复杂,但重复性极高。我想要的Agent,应该是输入一份配置文件,比如服务地址、日志路径、检查项清单,然后自主完成这些步骤:读取配置、整理任务列表、并行调用检查工具、分析结果、生成最终报告。
这个定位决定了它的体量:不需要重型的分布式系统,不需要复杂的编排平台,一个跑在终端里的单进程程序就够了,但要有下面几个能力:
- 能调用大模型来规划任务和解释结果;
- 能执行真实的工具函数,比如读文件、请求接口、执行命令;
- 能区分“规划”和“执行”,并且把每一步状态反馈给用户;
- 能把最终结论渲染成一个漂亮的终端报告。
这种体量,正好在“手写太繁琐”和“用重型Agent框架过重”之间。手动写,配置加载、参数清洗、日志埋点、函数注册这一整套骨架,至少浪费两天;直接上现成框架,又会被框架的设计习惯带跑,出了问题还得翻框架源码。所以我最终选择了一个折中方案:用AI编码助手快速生成骨架代码,我自己控制核心状态机。
1.2 为什么选择“让AI来写AI”
我见过不少人一听“AI写代码”就觉得不靠谱,但AI Agent这种项目的特性,恰好让AI生成代码的风险降到最低。
原因有三层。第一,Agent项目的核心不是算法,而是“胶水代码”:配置解析、工具函数注册、模型接口调用、结果汇总、格式转换。这类代码成熟度高、模式固定,AI生成出来几乎都是对的。第二,Agent架构天然适合“小步验证”,不会像大型业务系统那样,一启动就要面对几千条关联逻辑。写一个模块跑一次,出问题立刻能定位。第三,我要做的本来就是一个和AI交互的项目,提示词工程、函数调用schema、流式输出解析这些都是绕不开的,让AI写代码,等于提前把这些交互逻辑又演练了一遍。
当然,风险和收益一样明显。AI经常犯三类错:用了已经废弃的API、把接口参数想当然、在边界条件处理上一带而过。所以我的做法是:骨架代码可以给AI写,但每个模块都能单独运行,跑完一个我就手改一轮。项目最后的结构,大约七成代码是AI初稿,三成是我为了修坑和加“感应层”改出来的。
1.3 技术栈选型和理由
这个项目我用的核心搭配是这样的:
- 语言用Python,版本3.11往上。类型标注和泛型支持比老版本舒服太多。
- 配置和数据校验用Pydantic v2,所有工具函数的入参都定义成模型。这步在后面成了救命稻草。
- 大模型调用用的是某家的兼容接口,统一走函数调用格式。这样换模型服务商时,只有Base URL和API Key不同,代码不用动。
- 终端交互不用现成的富文本库,直接自己写ANSI转义渲染循环。这个决定让我吃了不少苦头,但也正是这个决定让我把光标bug的原理摸透了。
其实最初我也考虑过用现成的Agent编排库,但思来想去还是放弃。原因很简单:我要的不是“跑得通”,是“跑挂了我能知道为什么挂”。自建状态机虽然有重复造轮子的嫌疑,但所有日志、状态切换、决策路径都是我亲手埋的,出了问题能一路顺着栈查到底。这个选择后来被证明非常正确,尤其是做“感应模式”改造的时候,几乎没有任何框架惯性阻碍我。
2. API报错:从最初的手忙脚乱到建立一套处理框架
2.1 常见报错先分类,比一个个搜错误码更高效
我刚开始做这个项目时,一遇到API报错就复制错误信息去搜,结果搜回来的答案五花八门,很多还互相矛盾。后来我把一段时间内积累的报错全列出来,发现它们其实就四大类。
| 报错现象 | 大概率原因 | 修复策略 |
|---|---|---|
| 401 / 403 | API Key失效、权限不足、账号欠费 | 把密钥收进配置中心,启动时先做鉴权测试 |
| 429 | 触发限流、并发过高 | 指数退避加重试,限制同时进行的请求数 |
| 超时 | 网络抖动、单次请求过大、模型推理时间过长 | 适当调高超时上限,必要时拆小请求 |
| JSON解析失败 | 返回值被截断、模型生成了非法JSON | 预留二次修复调用,用强校验兜底 |
这四类其实对应着完全不同的处理思路。401是配置问题,跑不到重试就已经输了;429是流量问题,靠退避重试解决;超时是资源问题,需要考虑降级策略;JSON解析失败则直接指向模型输出质量,后面我会专门讲。
2.2 指数退避要加抖动,光有重试还不够
我当时处理429限流时,第一版写的是简单的“失败就几秒后重试”,结果发现并发一上来,进程间重试时间撞在一起,下游接口被压得更惨。后来才把所有重试逻辑统一成“指数退避+随机抖动”。
import time import random def call_with_retry(api_func, max_attempts=6): """ 统一API调用入口。 成功返回结果;失败持续重试,直到超过最大次数。 """ delay = 0.3 for attempt in range(1, max_attempts + 1): try: return api_func() except RateLimitError: wait = delay * (2 ** (attempt - 1)) + random.uniform(0, 0.6) logger.warning("第 %s 次触发限流,等待 %s 秒", attempt, round(wait, 2)) time.sleep(wait) except APITimeoutError: time.sleep(0.5) except UnauthorizedError: # 这种错误重试再多也没意义,直接抛出 raise raise RuntimeError("API调用超过最大重试次数")关键点有两个。第一个是抖动:如果不加随机值,多个并发请求的重试节奏会完全同步,形成“重试风暴”。第二个是要区分“可重试错误”和“不可重试错误”。像401这种,重试一百次也没用,最好的做法是启动时先做一次鉴权探测,失败就直接报错,而不是在任务运行到一半的时候才暴露。
我后来还把“背压”思想加了进去:当检测到连续429时,不是立刻重发请求,而是暂停新任务的分配,让整个Agent等一等。这就像水管已经有了压力,你还继续加泵,只会把接口打崩。实际效果是,加了背压之后,整个巡检耗时反而下降了,因为失败重试的次数少了很多。
2.3 最容易忽略的坑:工具调用参数的结构性错误
这个坑是我觉得API报错里最有含金量的一段。Agent读到LLM返回的“调用某个工具”指令时,LLM会生成工具名和对应参数。问题就在于,参数是模型生成的,类型、字段名、枚举值都可能不靠谱。
典型报错长这样:某个函数需要参数target_path,模型给成了path;某个枚举只接受low/medium/high,模型给成了Low;还有更离谱的,参数值本身是一个被截断的JSON,解析出来直接是None。
我最初的做法是在调用工具之前用json.loads解析参数,解析失败就让模型重新生成。但很快发现,这个方案在“参数类型不匹配”和“缺少字段”时毫无察觉,因为JSON本身是合法的,只是语义不对。
后来我改成方案:给所有工具调用参数建Pydantic模型,校验失败时把校验错误信息回传给模型,让它基于错误信息自我修复。
from pydantic import BaseModel, ValidationError class ReadLogParams(BaseModel): target_path: str max_lines: int = 100 def safe_tool_call(tool_name: str, args_json: str): schema = TOOL_SCHEMAS[tool_name] try: params = schema.model_validate_json(args_json) return execute_tool(tool_name, params) except ValidationError as exc: # 结构化错误信息,方便模型修正 errors = [{"field": e["loc"][0], "msg": e["msg"]} for e in exc.errors()] correction_prompt = build_correction_prompt(tool_name, args_json, errors) fixed_json = call_llm_again(correction_prompt) params = schema.model_validate_json(fixed_json) return execute_tool(tool_name, params)这个代码很短,但它把Agent的容错能力提升了一大截。模型第一次报错、第二次修正,大多数工具调用都能在三次内成功。最重要的是,整个过程是“显示反馈”,不像之前那样模型自己蒙头重试。
2.4 给每次API调用留下“可回放”的日志
这是我在API报错阶段积累的最重要习惯。每次调用大模型之前,把请求体、工具清单、超时时间、访问的目标函数都记录下来;拿到响应之后,再记录耗时、token消耗、返回结果截断摘要。这样一旦某个环节出了问题,我可以从日志里直接还原当时的上下文,而不是靠“刚才好像传了什么参数”这种模糊记忆。
{ "event": "llm_call_start", "timestamp": "2025-01-12T09:30:01.234Z", "call_id": "65f0ab4e", "model": "some-model-name", "tool_count": 8, "prompt_hash": "a1b2c3d4" }日志记录的密度要控制好,否则文件膨胀很快。我的规矩是:小字段全量记录,大字段只记哈希和截断摘要。出了事故需要排查时,再用日志里的call_id从缓存中取出完整请求体。
3. 终端光标bug:把简单输出逼成了渲染循环
3.1 为什么一个Agent要折腾终端交互
这个项目的输出端,最初就是print一行行往终端打。但跑了几轮之后,我意识到这是不够的:Agent执行一个较长的检查任务时,中间可能有十几秒没有输出,用户看起来就像是卡死了。而且巡检过程中有并行任务,多个任务的进度交错在一起,使用普通的print根本看不清。
于是我给Agent加了“终端面板”需求:顶部显示当前处于哪个任务阶段,中间显示最近几条进度日志,底下显示一个动态进度提示。理论不复杂,但做起来让我深刻地理解了“终端里的每一行,都是会呼吸的生命”。
3.2 光标bug三连:叠加、跳行、穿插
我踩的光标坑基本可以归纳成三个典型症状。
| 症状 | 触发原因 | 修复方式 |
|---|---|---|
| 进度条重复叠加,留下一串残影 | 只用了\r回到行首,但没有清除当前行,新内容叠在旧内容上 | 每次重绘前用\x1b[2K清除整行 |
| 多行面板上移后错位 | 用\x1b[1A上移N行,但实际内容因为换行占了N+1行,导致后面所有行全废 | 不依赖“上移N行”,改用清屏+定点重绘 |
| 业务日志和渲染面板互相穿插 | 业务线程直接print,渲染线程同时也在写终端,两边的输出混在一起 | 所有终端写入收敛到一个渲染循环 |
这里面的“元凶”其实是对终端控制序列的理解不完整。\x1b[1A代表“光标上移一行”,\x1b[2K代表“清除当前行”,\x1b[H代表“回到左上角”。单独使用都没问题,但一旦组合起来,任何一处行数计算偏差都会引发蝴蝶效应。
更要命的是终端的物理行和逻辑行不是一回事。你的内容如果超过终端宽度,终端会自动折行,一个逻辑行可能占两个物理行。特别在窗口宽度变化时,之前的“上移五行”可能对应到六七个物理行,光标位置瞬间就错乱了。
3.3 从“增量移动”转向“整区域重绘”
我后来彻底放弃“精细计算光标位置”的做法,换成更暴力的方案:把终端面板当成一块画布,每次更新状态时,清空整个画布区域,再完整重绘所有行。
import queue import shutil import sys import threading import time class TerminalUI: def __init__(self, refresh_interval=0.2): self._msg_queue = queue.Queue() self._status: dict[str, str] = {} self._lock = threading.RLock() self._running = True self._thread = threading.Thread(target=self._loop, daemon=True) self._thread.start() def update_status(self, key: str, value: str): with self._lock: self._status[key] = value def push_log(self, line: str): with self._lock: self._msg_queue.put(line) def _build_lines(self) -> list[str]: width = shutil.get_terminal_size().columns lines: list[str] = [] for key, value in self._status.items(): lines.append(f"{key}: {value}".ljust(width)) while not self._msg_queue.empty(): try: lines.append(str(self._msg_queue.get_nowait())[:width].ljust(width)) except queue.Empty: break return lines def _loop(self): while self._running: with self._lock: lines = self._build_lines() self._draw(lines) time.sleep(0.2) def _draw(self, lines: list[str]): with self._lock: sys.stdout.write("\x1b[?25l") # 隐藏光标 sys.stdout.write("\x1b[H") # 回到左上角 for line in lines: sys.stdout.write(line + "\x1b[K\r\n") sys.stdout.write("\x1b[0J") # 清空剩余区域 sys.stdout.write("\x1b[?25h") # 显示光标 sys.stdout.flush() def stop(self): self._running = False self._thread.join()这段代码有几个细节是反复调出来的。
第一,隐藏光标的转义序列必须放在绘制前,显示光标的序列放最后。否则用户会看到光标在重绘过程中满屏跳动。
第二,所有对终端输出缓冲区的操作都持有同一个锁,保证不会有两个线程同时写。
第三,为了兼容窗口宽度变化,每行输出前都通过shutil.get_terminal_size()获取当前宽度,再做截断和填充。这样即使终端被拉窄,也不会出现折行导致的错位。
这套方案牺牲了一点性能,但换来的是“输出永远完整”。进度消息由独立的刷新循环统一驱动,巡检业务线程只需要把新状态丢进队列,完全不碰终端输出。
3.4 终端调试实用技巧
如果你也在写类似的终端渲染,下面这几个调试姿势能省出好几个小时。
- 把输出重定向到文件,然后执行
cat -v 文件,可以看到所有转义序列的原始样子。这个方法能快速判断是不是转义代码写错了。 - 缩小终端宽度来触发折行bug。很多光标错位问题,在宽终端里看不出来,一拖窄就现形。用一个小循环不断改变窗口尺寸,能暴露各种边界问题。
- 先把渲染帧率调慢到1秒一次,方便肉眼观察每帧的差异。调好后再恢复速度。
- 千万记住:业务代码里别再用裸
print,所有日志都要走同一个渲染模块。只要有一个漏网之鱼,面板就会瞬间被搅乱。
4. 感应模式理论:从Bug里长出来的一套Agent设计观
4.1 一次让我印象深刻的“假成功”
项目跑到后期,功能基本都能跑通了,但出现了一个特别让人恼火的现象:Agent偶尔会在报告里写“服务状态正常”,可我手动检查发现服务明明已经挂了。
我一开始以为是模型幻觉或者提示词不清晰,反复调整提示词都不见效。直到我翻日志,发现了一个细节:那次巡检里,某个检查工具第一次调用超时了,Agent然后走了一个缓存路径,从内存里拿了一份旧的检查结果继续往下走。后续规划也没确认这份结果是不是最新,直接把它当成“最新状态”写进了报告。
这让我意识到,Agent真正缺失的不是推理能力,而是一种“对状态进行感知校验”的机制。它会读日志、会调接口、会执行命令,但它不会在执行动作之前确认“我掌握的这些状态是不是当下真实的状态”。
4.2 感应模式的定义:先感应,再执行
我后来把这个机制总结成一个框架,取名“感应模式理论”。核心思想很朴素:Agent的运行应该分为两种相互交替的模式。
感应模式(Sensing Mode)负责收集当前环境的事实信号,包括外部信号、内部信号和校验信号:
- 外部信号:文件是否存在、服务接口的返回码、日志时间戳、系统负载;
- 内部信号:当前上下文窗口剩余量、已执行过的工具列表、token预算;
- 校验信号:上一次工具调用的成功标志、传入参数是否与当前目标一致、结果缓存是否过期。
执行模式(Acting Mode)基于感应结果制定计划并行动。计划的每一步都绑定着明确的感应前提,前提不成立就绝不执行。
一个形象的类比是,你去推一扇看起来很重的铁门时,会先伸手碰一下,感受门的锁有没有解开,再决定是用力推还是找钥匙,而不是直接拿肩膀撞。那些明明应该先“摸门”却直接“撞门”的Agent,表现就会很飘。
你如果把它拆到代码层面,它其实就是一个显式状态机:状态从“感知”开始,感知结果喂给规划模块,规划产出动作,动作执行后必须触发下一次感知,如此循环。
4.3 把感应模式工程化:状态机与流程改造
我在Agent的主循环里直接加入了感应快照结构。
from dataclasses import dataclass, field @dataclass class SensingSnapshot: """ 感应模式产生的当前事实快照, 作为所有决策的输入底座。 """ timestamp: float tool_results: dict[str, object] stale_after: float = 30.0 facts: list[str] = field(default_factory=list) def is_stale(self, key: str) -> bool: result_time = self.tool_results.get(key, {}).get("fetched_at", 0) return time.time() - result_time > self.stale_after class AgentLoop: def run(self, user_goal: str): while not self.is_goal_done(user_goal): snapshot = self.sense() # 感应模式:收集事实 plan = self.plan(user_goal, snapshot) # 在事实约束下规划 for step in plan: if not self.check_sensing_precondition(snapshot, step): snapshot = self.sense() # 前提不成立,重新感应 executed = self.execute(step) self.observe(executed) # 执行结果回到观察通道这里最重要的改动是,规划不再是“凭感觉规划”,而是“基于感应快照规划”。任何缓存结果都会被标记上获取时间和来源版本号。当Agent想引用某条旧状态时,先检查是否过期,过期就重新感应。
感应模式的成本很低,就是一次状态扫描和几条查询,但它能挡住大量因为“状态不匹配”而产生的幻觉执行。
4.4 提示词层面的落地:让LLM先讲事实,再谈行动
我不能只在代码层面做状态机,因为决策最终是LLM做的,它必须理解这套模式。
所以我在系统提示词里加了一段固定格式:
在执行任何动作之前,你必须先进入感应模式,输出以下内容: 1. 当前任务对应的环境事实列表(文件状态、接口状态、时间戳)。 2. 这些事实中,哪些来自缓存,哪些来自实时获取。 3. 是否存在因果未知或信息缺失的问题。 只有当事实列表足够支持行动决策时,才能输出行动计划。 任何没有事实支撑的结论,都必须标记为“待验证”,禁止直接当作结果。这个提示词改动带来的效果非常直接:Agent的错误断言明显变少,而且日志里能清晰看到“这个结论是感应出来的,还是推断出来的”。有了这层标记,后续调优就变得有序了。
4.5 感应模式为什么能把前面所有问题串起来
回头看这个项目,API报错和终端光标bug,本质上都是“状态传递故障”。
API报错是外部状态没有进入决策通道。只重试但不感知失败原因、不改下次行为,Agent就是在瞎撞。把感应模式加上后,每次调用前先检查鉴权状态,每次失败后主动采集错误码信息,并且把这个信息推进下一轮规划,这样Agent的行为才真正闭环。
终端光标bug则是内部渲染状态和物理终端状态没有对齐。渲染循环的问题,其实就在于它没有“感应”终端当前宽度、当前光标位置和缓冲区内容,就开始冒失地输出。我最后采用的重绘方案,本质上就是“全量感知再输出”。
感应模式理论本质要解决的是同一个问题:系统的任何决策和输出,都必须建立在“对真实状态的最新感知”之上。没有这一层,无论模型多聪明,表现都会像不看路只闷头跑的人。
5. 完整复盘:三个被验证过的实操经验
5.1 第一个教训:最先跑通“最小闭环”,再让Agent变聪明
我最开始犯的错,是想着把所有内容都做好再跑。模块一个接一个写完,结果第一跑就发现不知道是配置加载错了、模型返回格式不对、还是渲染循环崩了,问题叠加成一大片,完全没法定位。
后来我砍掉一切花里胡哨,先做了一条最小路径:读配置 -> 调一次模型 -> 输出结果 -> 退出。这条路径的全部意义就是验证“骨架是通的”。它跑通了,我再往上面加工具调用、加自动重试、加终端面板。每加一个功能,就回归跑一次最小路径。
这招对AI生成的代码尤其重要。AI生成模块时基本不会考虑和你现有代码的耦合,它的局部实现可能完美,但和你已有的调用契约不一定匹配。通过频繁跑最小闭环,你能在“新的不匹配”出现的第一时间发现它,而不是攒到最后一锅端。
5.2 第二个教训:提示词里要写“禁止假设动作已执行”
和AI Agent协作时,最可怕的不是模型答错,而是模型“假装做过”。
有一次我让Agent检查一个日志文件是否包含某个模式,它直接返回“模式存在”,但实际上那一次调用里它并没有真正打开文件,而是根据上下文猜了一个结论。为了避免这种问题,我在系统提示词里加了一组硬约束:
- 禁止在未调用工具的情况下描述工具执行结果。
- 每个动作必须附带一个可验证的指标:文件路径、HTTP状态码、命令退出码。
- 所有无法验证的推断,必须显式标注为“推测”。
这段约束看起来简单,但是效果极好,因为LLM很擅长顺着指令办事,你给了“可以推测”的口子,它就会肆无忌惮地推测。把口子堵死,行为立刻规矩很多。
5.3 第三个教训:日志是Agent的外部器官
没有日志的Agent,就像没有仪表盘的车。你可以把车开动,但你不知道转速、油量、水温,出了刺耳的异响也只能瞎猜。
这个项目的日志体系经历了三次演进。第一阶段,到处print,混乱而且定位问题靠运气。第二阶段,引入logging模块,按级别输出,可读性好了但无法检索。第三阶段,改成结构化事件日志,每行一个JSON事件,带事件类型、时间戳和关键字段。
一个值得分享的细节是:事件日志里的字段,尽量记录“机器可读”的值,而不是人类自然语言的描述。比如"check_result": "failed"比"log": "服务好像挂了"有用得多。前者可以直接用脚本统计分析,后者只适合人肉看。
5.4 AI生成代码的专项审查清单
项目收尾时,我总结了一份给AI生成代码用的审查清单。CI或者CR带上这份清单能少踩很多坑。
| 审查项 | 为什么会出问题 | 怎么验证 |
|---|---|---|
| 依赖库版本是否与项目锁定一致 | AI容易默认最新版API | 启动时打印依赖版本,跑一次最小闭环 |
| 接口入参是否经过schema校验 | AI常写裸参数裸传 | 所有入参定义Pydantic模型 |
| 错误处理是否会吞异常 | AI喜欢except: pass | 检查所有except块,至少留一条日志 |
| 是否有真实执行还是只是模拟 | AI偶尔生成mock代码 | 用测试数据跑真实调用 |
| 日志是否足够支持回溯 | AI默认不写关键事件 | 关键路径加结构化事件日志 |
这份清单现在也用在我其他项目上,不管代码是AI写的还是人写的,都成立。
6. 常见问题速查:把踩过的坑变成一张表
6.1 API调用问题速查
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 报401但密钥刚生成 | 密钥传错环境变量或配置文件 | 写一个自检命令,单独验证密钥连通性 |
| 偶发抖动式超时 | 并发过高或网络抖动 | 超时重试+限制最大并发数 |
| 连续429但重试无效 | 退避时间不够久 | 把最大等待时间提高到30秒以上,并加抖动 |
| 返回200但JSON解析失败 | 流式响应被截断 | 改为非流式,或者解析前校验结束标记 |
| 工具函数收到错误参数 | 模型生成了结构合法但语义错误的参数 | 用强校验模型兜底,把错误回传给模型修正 |
6.2 终端渲染问题速查
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 内容反复叠加出残影 | 清除行序列没写或顺序不对 | 绘制前写\x1b[2K |
| 面板位置越画越往下 | 逻辑行宽度超过终端列宽 | 每行按终端列宽截断 |
| 日志和面板混在一起 | 业务线程直接打印 | 统一走渲染线程队列 |
| 光标不停闪烁 | 没有隐藏光标 | 绘制开始前\x1b[?25l,结束\x1b[?25h |
| 调整终端大小后混乱 | 重绘时没有重新读取宽度 | 每帧刷新shutil.get_terminal_size() |
6.3 Agent行为问题速查
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| Agent声称成功但实际没做 | 没经过感应就直接行动 | 在提示词中加入“禁止假设动作已执行” |
| 反复调用同一工具 | 结果缓存未标记时间,模型不知道已取过 | 缓存字段加fetched_at |
| 输出报告内容陈旧 | 引用了过期工具结果 | 执行前检查快照过期时间 |
| 任务卡在重试循环里 | 请求schema错误导致模型自己修不好 | 手动检查该工具的Pydantic模型定义 |
我在实际使用中的体会是,Agent项目最难的从来不是“调通一个API”,而是把决策、执行、感知三条链路理顺。API报错看起来是网络问题,本质上是状态流转失败;终端光标bug看起来是渲染问题,本质上也是状态同步失败;Agent产出假结论看起来是模型问题,本质上还是状态感知缺失。
最后再说一个我在这个项目里收获不小的小习惯:无论做什么自动化项目,我都习惯先在代码里画一条“感应-规划-执行-复盘”的循环骨架。先用最笨的方式把事实采集做扎实,再让AI承担它擅长的推理部分。这个顺序一旦反了,后面补的东西会越来越多,而且永远补不完。