你有没有发现,身边很多人把ChatGPT用成了"高级搜索引擎"——问它问题、让它写文案,它确实很能说,但要它"真的动手做点事"就卡住了。让它整理一下指定文件夹里的文档、把一堆聊天记录按议题归好类、每天自动跑一遍数据检查,它只会给你一堆建议,然后等你手动执行。
这中间的落差,就是ChatGPT和AI Agent的区别。
我花了大概三周时间,从一个只会对话的模型用户,慢慢改成能自己动手干活的AI助手。这篇就来完整拆一遍:概念上ChatGPT和Agent差在哪,Agent的核心机制是什么,怎么一步步做出一个能真正操作文件、执行任务的最小系统,以及跑起来之后那几个让我挠头很久的坑。
如果你也想把你手头的模型从"聊天窗口"升级成"数字员工",这篇应该能让你少走不少弯路。
1. 先把概念掰开:ChatGPT为什么不能直接"动手"
大多数人对ChatGPT的认知是:我说话,它回答。这本质上是一个问答闭环。模型拿到你的输入,生成一段文字,结束。它不知道你电脑上有什么文件,不能帮你改配置,更不会在你睡觉的时候替你巡检服务器。
AI Agent则不一样。它不是一个单次问答,而是一个能持续运行的任务执行系统。
我习惯用一个厨房的类比来解释:ChatGPT像一位站在旁边只看不动手的顶级厨师,他知道菜谱、知道火候、知道该放多少盐,但不会真的碰锅铲。AI Agent则是你雇的一个后厨团队——它能自己看冰箱里有什么食材(感知环境)、决定先切菜还是先烧水(做决策)、实际操作锅碗瓢盆(执行动作),然后尝一口觉得咸了再加点水(根据反馈调整),直到最后端出一盘菜(完成任务)。
所以"能动手"这三个字,拆开来看其实是四个环:
- 感知:获取任务环境里的信息,比如扫描一个目录、读取一份文件内容。
- 决策:根据信息判断下一步做什么,这依赖大模型的推理能力。
- 行动:调用具体工具,比如移动文件、执行代码、发送请求。
- 反馈闭环:观察行动结果,判断是否完成,没完成就继续循环。
如果你只是用ChatGPT网页版,你只享受了"决策"这一环。一个能动手的Agent,需要把另外三环补齐。
再直白一点:你让ChatGPT"帮我把下载文件夹里的图片全部转成PDF",它做不了,因为它没有你的文件系统权限。但你给Agent挂上文件操作工具,它就能自己去扫描、转换、输出,然后告诉你"转好了,放在这个位置"。
这就是从"会说话"到"能干活"的本质跨越。
不过这里得提醒一句:很多人会觉得Agent很玄乎,其实它底层还是那个大模型,只是我们给它加了一双手和一套行动规则。理解这一点,下面所有技术细节就都顺了。
2. Agent能被你指挥的底层秘密:四件套缺一不可
想把Agent做好,绕不开四个核心机制。我分别说一下它们是什么、为什么存在,以及少了它们会怎样。
2.1 推理模式:让它"边想边干"
多数Agent框架用的推理模式叫ReAct,全称是Reason and Act,翻译过来就是"思考然后行动"。
它的工作方式我不绕弯子,直接看一个例子。假设任务是把某个文本文件里的错别字找出来:
- 第一轮:模型的思考是"我需要先看到文件内容,因为我无法凭空知道里面写了什么",于是它输出"调用read_file工具,参数是路径"。
- 系统执行这个工具,把文件内容返回给它。
- 第二轮:模型看了内容,思考"发现问题了,现在要把修改建议写到一个新文件里",于是输出"调用write_file工具"。
- 系统写入文件,返回成功。
- 第三轮:模型确认结果,输出最终答案。
你发现没有,这不就是我们人类干活的方式吗?先想、再做、看结果、再想。模型中每一步都会暴露在它的输出里,你甚至能全程看它"自言自语"。
这个模式最妙的地方在于:它让模型不会一次性编造一个完整答案,而是让模型基于真实的中间结果逐步逼近目标。如果没有这个循环,你让模型处理文件,它大概率会直接给你编一份"看起来像真的"的内容。
2.2 工具调用:给模型装遥控器
大模型本质上是一个文本生成器,你再怎么调,它也没法直接操作你的电脑。所以必须有一个中间层:让模型输出指定格式的指令,由系统解释并执行。
我通常把工具调用协议设计成JSON格式,比如:
{ "thought": "我需要查看目录下的文件列表", "action": "list_files", "action_input": { "path": "/home/user/downloads" } }模型输出这段JSON,你的代码解析它,调用对应的函数,然后把结果返回给模型。这就像一个遥控器:模型不直接按物理按键,而是"说出"要按哪个键,系统替它按。
这一段是Agent系统的核心桥梁,后面第四章我会给出完整代码。
2.3 记忆系统:短期上下文和长期存储
Agent是循环执行的,每一步都会把历史信息带进下一次请求。这些历史信息就是短期记忆。但模型的上下文窗口有限,任务一旦复杂,前面说的内容就会被挤掉。
所以记忆系统最少要有两层:一是把最近几轮对话完整保留,二是把更早的内容做摘要压缩,或者把关键信息写入外部文件、向量库,需要时再检索出来。
我最初就栽在这里:让Agent处理几十个文件,跑到第15步时它开始"忘了"最初的任务要求,行为明显漂移。后来把任务约束写进系统提示词,并且每隔三轮做一次进度摘要,情况才稳定下来。
2.4 执行反馈:允许犯错,但要让错误可见
Agent执行过程中一定会出错。工具返回一个错误信息,不应该让流程崩溃,而应该把这个错误信息原样喂回给模型,让它自己调整策略。
比如文件移动失败,是因为目标目录不存在,Agent看到错误后就会想着先创建目录再移动。这种"从错误中自我修正"的能力,是Agent比固定脚本聪明的地方——脚本遇到意外只会报错退出,Agent会自己找路。
所以,你在设计工具函数时,返回值里一定要包含足够清晰的错误上下文,比如"Permission denied: /xxx",模型才能据此做下一步判断。吞掉错误信息,等于让Agent瞎跑。
3. 零代码阶段:先让对话产品干一点真活试试水
很多人一看要写代码就退缩了。其实在完全手写Agent之前,你可以先用现成的对话产品,体验一下"给模型加规则和操作边界"是什么感觉。
现在的ChatGPT这类对话产品,普遍支持自定义指令或者自定义助手功能。你可以把它理解为一个"有固定岗位说明书"的对话版Agent——它还是不会替你操作电脑,但能在你的规则约束下,稳定地完成信息处理类任务。
我做了两个小实验,你可以直接抄:
第一个是"会议记录整理助手"。我在自定义指令里这样写:
你是会议记录整理助手。收到原始记录后,先按时间线拆分议题;每个议题提炼背景、结论、负责人、下一步截止时间;如果原文信息缺失,标注"信息缺失"而不是自行编造;最后输出一份Markdown表格。
然后我把一段混乱的语音转文字丢进去。效果比默认模式好非常多:它不会自由发挥补充细节,不知道的就是不知道,而且格式稳定,几乎不需要二次修改。
第二个是"邮件分类回复助手"。规则是:先判断邮件紧急程度(今天截止/本周/可稍后回复),再给三个候选回复语气(正式、简洁、亲和),最后必须列出一个"需要用户确认"的清单。
这两个小工具让我第一次直观感受到:模型能力强不强是一回事,你怎么用规则约束它是另一回事。同样的模型,配合一套好指令,产出质量能提升一个档次。
但用了一周之后,我明显碰到了天花板。三个瓶颈特别突出:
- 无法对接本地数据:它读不到我电脑上的文件,所有内容都要手动粘贴进去。
- 不能定时自动执行:不能设置"每天晚上七点自动跑一遍",必须手动对话。
- 操作范围固定:它能处理文本,但不能移动文件、不能运行代码、不能调用其他软件。
到这个阶段,我再回头看,就明白了为什么必须要学一点代码,写一个自己的Agent。别怕,代码量没有你想象的那么吓人。
4. 手写最小Agent系统:这次的代码真的能跑
我不建议新手一上来就套用重型框架。先手写一个最小可用的Agent循环,把原理跑通,后面再用框架都来得及。下面这个系统,我把它作为一个模拟项目,全部代码加起来不到150行。
4.1 整体架构:一个循环打天下
整个Agent就是下面这个循环:
- 把任务、工具描述、历史记录拼成提示词。
- 发给大模型,让它输出JSON格式的指令。
- 解析指令,执行对应工具函数。
- 把工具返回值追加到历史记录。
- 重复,直到模型输出"finish"指令或超过最大步数。
这看起来简单,但它是所有Agent系统的骨架。先把这个跑通,你再去理解那些重框架会轻松得多。
4.2 工具层的实现
工具层可以设计成一个注册表。我用一个字典把工具名映射到函数,每个工具附一段自然语言描述,让模型知道什么时候该用它:
TOOLS = {} def register_tool(name, description): def decorator(func): func.description = description TOOLS[name] = func return func return decorator @register_tool("list_files", "列出指定目录下的所有文件和文件夹,参数path为目录路径") def list_files(path): import os return "\n".join(os.listdir(path)) @register_tool("read_file", "读取文本文件的内容,参数path为文件路径,max_length为最大读取字符数") def read_file(path, max_length=2000): with open(path, "r", encoding="utf-8") as f: return f.read()[:max_length] @register_tool("write_file", "将内容写入文件,参数path为文件路径,content为写入内容") def write_file(path, content): with open(path, "w", encoding="utf-8") as f: f.write(content) return f"写入成功: {path}"设计上最重要的一个细节是:每个工具都有一段清晰的描述。这不是给人看的,是给模型看的。模型依靠这些描述决定何时调用哪个工具。描述写得越清楚,模型用错工具的几率越低。
4.3 主循环:Agent的大脑
接下来是主循环。大模型API的调用我做了一层封装,不同服务商的SDK不同,你换成自己用的那个就行:
def call_llm(messages): # 这里替换为你所用的大模型服务的SDK调用 # messages是一个消息列表,返回纯文本字符串 response = your_llm_sdk_call(messages) return response主循环代码如下:
SYSTEM_PROMPT = """你是一个可以操作本地工具的AI助手。 你可以使用的工具如下: {tool_descriptions} 你的回复必须严格是JSON格式,包含以下字段: - thought: 你对当前情况的思考 - action: 要调用的工具名,或"finish" - action_input: 工具参数,字典格式 - result: 当action为finish时,给用户的最终答案 一次只能调用一个工具,等待工具结果后再决定下一步。""" def run_agent(task, max_steps=8): tool_descriptions = "\n".join( f"- {name}: {func.description}" for name, func in TOOLS.items() ) messages = [ {"role": "system", "content": SYSTEM_PROMPT.format(tool_descriptions=tool_descriptions)}, {"role": "user", "content": task}, ] for step in range(max_steps): response = call_llm(messages) action = parse_json_response(response) if action["action"] == "finish": return action.get("result", "任务完成") tool_func = TOOLS.get(action["action"]) if tool_func is None: observation = f"错误:不存在工具 {action['action']}" else: try: observation = tool_func(**action.get("action_input", {})) except Exception as e: observation = f"调用出错:{str(e)}" messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": f"工具观察结果:{observation}"}) return "超过最大步数,任务未完成"这里有两个容易忽略的细节。第一,max_steps必须设置。没有步数上限,模型可能陷入死循环,白白烧你的API额度。我自己跑的时候设过15,结果模型连续调了十几次同一个工具,最后才意识到该收敛。第二,工具调用出错时,不要把异常直接吞掉,而是像上面那样把错误信息变成观察结果返回给模型,让它自己想办法。
我把这个系统跑起来之后,第一个让它做的任务是:"在当前目录下创建一个notes文件夹,然后在里面写入一个文件,内容是今天日期和一句话总结。"
它中间的推理轨迹大致是:
- 第一步思考:"我应该先看看当前目录下有没有notes文件夹,所以先列出目录内容",然后调用
list_files。 - 第二步思考:"没有notes文件夹,我先创建它。但没有create_folder工具,我先用write_file直接写入到notes路径下,write操作会自动创建父目录",然后调用
write_file,内容包含当天日期。 - 第三步思考:"文件已经写入成功,我确认一下最终结果",输出finish。
看到这段轨迹的时候,我还是挺惊讶的。模型遇到"没有创建文件夹工具"的情况,自己找到了绕路方案。这就是Agent最迷人的地方——它不是机械执行固定代码,而是真的有"想办法"的能力。
4.4 如何调参:temperature和max_steps的取舍
在主循环里,我建议调用大模型时把temperature设低一些,0到0.3之间。原因很简单:Agent执行任务需要的是稳定和准确,不需要天马行空的创意。temperature太高,模型可能输出各种奇怪的JSON结构,解析都会出问题。
max_steps的设置则取决于任务复杂度。纯问答任务,3步足够。涉及多文件操作,10步是常见的。我一般从8起步,跑挂了再往上加。设得太小任务完不成,设得太大模型容易原地打转。
4.5 让Agent能执行代码:双刃剑
我给这个模拟项目额外加了一个执行Python代码的工具。这个工具的价值极大,因为有了它,Agent几乎能做任何事:批量改文件名、算数据、爬网页。
但风险也极大。模型对工具的调用权限,本质上等于你的执行权限。你让它执行代码,它就能删除你的文件,只不过目前大模型还不太会主动干这种坏事,但你要养成好习惯。
我在这个工具里做了最简单的防护:默认只允许执行代码,但禁止包含os.remove、shutil.rmtree、subprocess这类危险的调用,一旦检测到就返回警告。更成熟的方案是把Agent放进沙箱或者容器里跑,这是后面进阶要做的。
5. 实测一个完整任务:让Agent自己收拾乱糟糟的下载目录
代码跑通之后,我被问得最多的一个问题就是:"所以呢?它能干啥实际活儿?"
这里就演示一个足够有代表性的任务:整理下载目录。
这个目录的真实状况是:文件全堆在一起,有PNG截图、PDF文档、压缩包、临时文件、名字乱七八糟的安装包。要求是:按文件类型分到不同子目录,对相似命名的文件统一重命名,最后输出一份整理报告。
如果我用传统脚本,得先手动摸清所有文件情况,再写一堆if判断。但用Agent的方式,我只需要把任务用自然语言描述出来:
请扫描当前目录,把所有文件按类型分类:图片放images,文档放documents,压缩包放archives,其他放misc。分类时优先根据文件扩展名判断。另外,如果发现文件名里有明显的乱码或日期戳,保留日期部分重命名为易读格式。完成后生成一份report.md,说明每个文件的最终去向。
它实际跑的过程我截取了关键几步,你可以感受一下这个"边看边干"的节奏:
第一步,它调用list_files,返回了一长串文件名。
第二步,它的思考是"文件太多了,我需要先读取完整文件列表,再逐个决定类型。图片扩展名有png和jpg,文档有pdf和docx,压缩包有zip和rar。我先统计一下当前有哪些扩展名",然后调用了一个统计扩展名的工具。
第三步,它开始规划目录结构,创建了四个子目录。
第四步到第十步,它逐个移动文件,中途遇到一个0字节的空文件,它的思考是"这个文件没有内容,无法判断类型,我把它放到misc目录并做标记"。
最后,它写入了report.md,内容里甚至加上了一句"根据文件名判断,report_final_v3.pdf和report_final_V4.pdf疑似同一文件的重复下载,建以只保留一个,请确认"。
这个案例能说明三件事:
- Agent能处理没有预先定义好规则的情况。空文件、乱码命名、疑似重复,这些在传统脚本里都是要人肉先发现再处理的边界情况,模型在运行中自己判断了。
- Agent会主动暴露风险和需要人类确认的事项,而不是闷头处理完拉倒。
- 你不需要维护一套复杂的if-else逻辑,只需要用自然语言描述目标,剩下的交给模型推理。
当然,它也有跑得慢、偶尔判断不准的时候。比如有一份PDF它根据文件名猜它是合同,其实只是随手保存的网页打印版。这种语义误判不可避免,因为它是"猜",不是"确定"。所以我在任务描述里刻意加了"分类时优先根据扩展名",把人的规则先立住,减少模型的自由发挥空间。
6. 运行不稳?五个高频故障的完整排查记录
把Agent从demo推进到日常使用,中间肯定会遇到各种"不听话"的情况。我把自己踩过的坑逐个复盘一下,每个都给完整的排查链路,方便你对号入座。
6.1 模型说"我看到了",但实际内容被截断了
现象:Agent在处理大文件时,明明已经调用了read_file,但后续行为明显没基于真实内容,像是瞎编的。
排查过程:我先把工具的返回值打印出来,发现当文件超过2000字符时,返回内容被截断了,但返回值里没有任何截断标记。模型并不知道自己只看到了一部分,它以为那2000个字符就是全部。
修复方案:给read_file加两个能力。一是返回值里明确注明"内容已截断,总长度XXXX字符,如需继续阅读请指定读取范围"。二是支持offset参数按段读取。这样模型就有能力把长文档分多次读完。
这个坑的本质是:工具返回信息的完整性直接影响模型判断。在设计工具时,信息完整性比信息量更重要。让模型知道自己还有多少内容没看,比一股脑给它更长的内容更管用。
6.2 循环失控:同一工具被反复调用
现象:Agent在某一步突然开始疯狂调用list_files,连续四五步做的都是同一件事,明显在原地打转。
排查过程:我把每步的JSON指令都打印出来,发现模型的thought字段显示它"试图确认当前文件状态",但完全不记得自己刚才已经确认过了。这是因为它把前面几轮的信息挤出了有效上下文。
修复方案有两层。第一层是在系统提示词里明确加上一句:"如果你已经调用过某个工具,不要立即重复调用;如果需要刷新信息,请说明原因。"第二层是写了一个检测逻辑:如果连续三步调用同一工具且参数相同,就主动打断,在消息里提醒模型"你已经执行过相同操作,结果没有变化,请改变策略"。
后来我又想到一个更根本的解法:给每个工具返回加一个序号标记,让模型能一眼看出"这是第N次得到相同结果",这个小小的提示就能大幅减少原地打转。
6.3 上下文爆炸:任务越跑越"失忆"
现象:一个任务里有大量小步骤,跑到十几轮之后,Agent开始遗忘最开始的约束条件,甚至试图去操作无关目录。
排查过程:查看每次请求的token数量,发现到第10轮左右,单次请求已经非常膨胀,早期关键的细节被大量工具结果淹没了。
修复方案:做一个简单的记忆压缩机制。每3轮做一次摘要,把前3轮的记录压缩成一条简短的进度说明,替换掉原文。同时把核心任务要求固定放在系统提示词里,不管历史怎么滚,最初的约束不会被冲掉。
之后我把任务描述再做了一次调整:明确告诉模型"每一步操作前,先核对是否偏离了最初的目标"。这句话看似简单,但对保持行为一致性很有帮助。
6.4 越权操作:它真的会去删文件
现象:有一次我在测试日志清理任务时,Agent自己决定直接删除一个日志文件所在的目录,而不是我想象中的只清理过期文件。删除操作非常果断,根本没有问我。
排查过程:回看整个决策链,发现问题出在工具描述上。我给的delete_file工具描述写的是"删除指定文件",而当时模型认为目录里的旧日志"应该清理掉",于是调用了删除工具。它没有"删除前需要确认"这个意识,因为我没教。
修复方案:分了两步。一是把带有破坏性的工具全部收进一个"危险操作区",默认情况下Agent如果要调用,必须先调用一个request_confirmation工具征求授权,而不是直接执行。二是在每个危险工具的描述开头都加上警告:"此操作不可恢复,仅当用户明确要求删除时才使用。"
这个坑给我最大的教训是:Agent的权限边界,必须从工具设计层面约束,不能指望模型自觉。模型天然倾向于"完成任务",不带任何安全意识。你给它一个删除按钮,它就真敢按。
6.5 幻觉修补:工具返回空,它给自己编一个
现象:有一次read_file返回了空字符串(文件本身是空的),Agent在下一步的思考里居然写"文件内容如下:xxxx",编了一段不存在的原文。
排查过程:单看最终输出时,根本发现不了问题。我对比了工具实际返回值和模型后续引用,才发现内容对不上。
修复方案:工具层做了两条规则。一是返回值如果为空,明确返回"警告:文件内容为空,没有任何文字内容",而不是返回空字符串。二是模型侧的系统提示词里强调:"当工具返回为空或警告时,必须如实报告,不得自行生成或猜测相关内容,如果你不知道就明确说不知道。"
这个坑在文本处理类任务里尤其危险,因为模型太善于"补全"了。真实内容缺失时,它会用概率判断"这里应该是什么",从而伪造内容。唯一的解药是让工具返回值边界极其清晰,同时逼模型时刻区分哪些是"观察到的",哪些是"推测的"。
7. 还能往哪里扩展:多Agent、定时任务与私有知识库
最小系统跑通之后,你可以在三个方向上升级,这几个方向也足够你研究很久。
7.1 多Agent协作:主管拆活,专员干活
单个Agent处理一个复杂任务时,容易上下文爆炸、职责混乱。我的做法是改成"主管-专员"模式:一个主管Agent负责理解用户意图、拆解子任务、分派给不同专员Agent;每个专员Agent只负责一个领域,让它们各自持有专用工具。
举个例子,做一个"月度数据报告生成器":主管Agent把任务拆成"数据统计"和"报告撰写"两个子任务,分别交给统计专员和写作专员。统计专员持有数据库查询工具,写作专员持有模板和文档生成工具。模块之间通过消息队列传递结果。复杂度上去了,但每个环节都非常可控。
7.2 定时任务化:Agent从"手动召唤"变成"自动值守"
我的做法很简单:把Agent的入口从命令行改成脚本,然后用操作系统的计划任务每天定时触发。比如每天下班前自动跑一遍日报汇总Agent,把当天提交记录、异常日志、项目进度汇总成一份文件发到指定的共享目录。
这一步改造的价值很实际:Agent只有自动化之后才算真正"能动手",不然它永远需要你按一下开关。
7.3 接入私有知识库:让Agent真正懂你的业务
基础的Agent只知道通用知识,但你的项目、你的历史决策、你的文档规范都不在它的知识范围内。我给它加了一个轻量的检索增强组件:把内部文档切片后向量化存起来,Agent在执行前先检索相关片段作为参考上下文。
这一步的价值立竿见影。同样是"帮我想一句对外公告",没有知识库时它写得漂漂亮亮但是空泛,有知识库之后它知道你们公司一贯的措辞风格和项目代号含义,写出来的东西才真正能直接用。
7.4 我做这几个实验之后的一点体会
最后说一点特别个人的东西。在折腾Agent这一个月里,我绕过的最大弯路是:一开始看各种框架的文档,被抽象概念淹没,觉得自己永远学不会。后来决心什么都不管,从头手写这个循环,反而几小时就通了。
你如果真的想要一个能动手的AI助手,我强烈建议你照着第四章这个最小系统先跑起来,哪怕只加一两个简单的工具。当你看清了"模型输出JSON、系统执行函数、结果再喂回模型"这个循环之后,再回头看市面上那些框架,你会发现它们的底层都是这一套东西。
跑通之后你就会知道,最难的不是写代码,而是想清楚:你希望Agent在那个场景下拥有哪些权限、遵守哪些边界、在什么情况下必须回头问你。这些边界想得越清楚,Agent就越可靠。