同一个Agent写三遍:直接调API、AgentCore与Claude Code插件落地对比
1. 同一个需求,三种做法:我为什么要把一个 Agent 写三遍
去年年底我手上有个挺具体的需求:每天定时抓取几个技术社区的热帖,做一轮摘要和分类,然后把结果推送到我自己的知识库里。需求本身不复杂,但我想借这个机会把当下主流的三种 Agent 落地路径都跑一遍,看看它们各自的手感、坑点和适用边界到底在哪。于是就有了这个标题——同一个 Agent,我用直接调 API、上 AgentCore、做成 Claude Code 插件这三种方式各实现了一遍。
先说结论性的体感:这三种做法不是替代关系,而是三种不同抽象层级的取舍。直接调 API 最裸、最可控,但你要自己管上下文、管重试、管工具调用循环;AgentCore 这类框架帮你把"Agent 运行时"这层封装好了,你专注写业务逻辑,代价是要接受它的约定和黑盒;做成 Claude Code 插件则是把 Agent 嵌进你已有的工作流里,它不再是独立程序,而是你日常敲命令时顺手就能触发的一个动作。
这篇文章适合谁看?如果你正在纠结"我的 Agent 到底该用什么方式落地",或者你已经写过一版调 API 的脚本但觉得维护起来越来越累,又或者你天天用 Claude Code 但还没试过给它写插件,那这篇应该能帮你少走点弯路。我会把三版实现的完整思路、关键代码、参数选择理由、以及我踩过的坑都摊开讲,你可以直接抄作业。
需要提前说明的是,文中涉及的具体框架版本和 API 形态会随时间变化,我尽量讲清楚"为什么这么选"而不是死记某个参数,这样即使接口变了,你的判断逻辑还在。
2. 先想清楚:Agent 和普通脚本的分界线在哪
2.1 什么情况下才需要 Agent,而不是一个 for 循环
很多人一上来就说要做 Agent,但其实一半的需求用普通脚本加几个 if-else 就够了。我判断的标准很简单:如果任务的执行路径无法在写代码时完全确定,需要模型在运行时根据中间结果决定下一步做什么,那才需要 Agent。
拿我这个抓取摘要的需求举例。如果我只是固定抓 5 个网站的固定栏目,那一个 requests 循环加一次摘要调用就完事了,根本不需要 Agent。但实际情况是:热帖的入口页面结构会变,有些帖子需要点进详情页才能拿到正文,有些帖子是视频需要走另一条处理路径,摘要完之后还要根据内容判断该归到哪个分类、要不要触发二次检索补充背景。这些决策依赖上一步的输出,写死在代码里会非常脆弱,这才是引入 Agent 循环的理由。
所以第一版我给自己定的目标很明确:实现一个最小的 ReAct 循环——模型输出思考、决定调用哪个工具、拿到工具结果、再决定下一步,直到它认为任务完成。这个循环不依赖任何框架,纯手写,目的是把 Agent 的"骨架"摸清楚。
2.2 三种落地方式的抽象层级对比
在动手前我先画了个心智模型,把三者的抽象层级摆清楚,这决定了后面每一版的代码量和可控性。
| 维度 | 直接调 API | AgentCore 框架 | Claude Code 插件 |
|---|---|---|---|
| 抽象层级 | 最低,全手写 | 中,运行时托管 | 高,嵌入宿主工具 |
| 上下文管理 | 自己维护消息数组 | 框架托管 | 宿主托管 |
| 工具调用循环 | 自己写 while | 框架内置 | 宿主内置 |
| 可控性 | 最高 | 中 | 受宿主约束 |
| 上手成本 | 中(要懂协议) | 低 | 低(但要懂插件规范) |
| 适合场景 | 深度定制、性能敏感 | 快速搭业务 Agent | 融入日常开发流 |
这张表是我做完三版之后回头补的,但当时心里已经有个大致判断:直接调 API 是"理解原理"的必经之路,AgentCore 是"工程化落地"的省力选择,Claude Code 插件是"个人效率工具"的最优解。三者对应三种不同的使用场景,没有谁绝对更好。
3. 第一版:直接调 API,把 Agent 循环的骨架摸透
3.1 为什么第一版一定要手写,不用框架
我的习惯是,任何新技术栈,第一遍一定手写最小实现。原因很实在:框架帮你省掉的东西,恰恰是你出问题时最需要理解的东西。如果我一上来就用 AgentCore,当 Agent 陷入死循环、或者工具调用参数总是错的时候,我根本不知道是框架的锅还是我的锅。
手写这一版,我要自己解决四个核心问题:消息历史怎么存、工具怎么定义和分发、循环什么时候终止、错误怎么处理。把这四个问题解决一遍,后面用任何框架我都能一眼看穿它在干什么。
3.2 消息历史与工具定义的最小实现
消息历史我用一个 list 存,每条消息是{"role": ..., "content": ...}的结构。这里有个新手常踩的坑:工具调用的结果必须以特定角色回填到历史里,而且顺序不能乱。模型发起工具调用时,返回的消息里带一个 tool_calls 字段,你执行完工具后,要把结果作为一条 role 为 tool 的消息追加进去,并且带上对应的 tool_call_id,否则下一轮模型会报错说找不到对应的调用。
工具定义我用 JSON Schema 描述,每个工具包含 name、description、parameters 三部分。description 这块我花了最多心思,因为它直接决定模型会不会在正确的时机调用正确的工具。我的经验是:description 要写清楚"什么时候用"而不是"这个工具是什么"。比如抓取工具,我写的是"当需要获取某个 URL 的正文内容时调用,输入必须是完整的 http 或 https 开头的地址",而不是干巴巴一句"抓取网页"。
tools = [ { "type": "function", "function": { "name": "fetch_page", "description": "当需要获取某个URL的正文内容时调用,输入必须是完整URL", "parameters": { "type": "object", "properties": { "url": {"type": "string", "description": "完整URL"} }, "required": ["url"] } } } ]3.3 循环终止条件与错误兜底
循环终止我设了三个条件,缺一不可:模型返回的消息里没有 tool_calls(说明它认为任务完成)、达到最大轮数上限(我设的 15 轮)、单轮耗时超过阈值。第二个条件是防死循环的保险丝,我实测下来 15 轮对这类任务足够,超过基本就是模型卡住了。
错误兜底这块踩过一个大坑。最开始工具执行抛异常我直接让程序崩了,后来改成把异常信息作为工具结果回填给模型,让它自己决定要不要换个方式重试。这个改动让整个 Agent 的鲁棒性上了一个台阶——比如某个 URL 抓取失败,模型会自己尝试换一个入口,而不是整个任务挂掉。
提示:工具执行失败时,把错误信息结构化后回填给模型,比直接抛异常终止要好得多。但要注意别把敏感的内部错误堆栈原样塞进去,做一层清洗。
这一版跑通之后,我对 Agent 的理解从"概念"变成了"手感"。但问题也很明显:代码量不小,光是消息管理和工具分发就写了三百多行,而且每加一个工具都要改分发逻辑。这就引出了第二版。
4. 第二版:上 AgentCore,把运行时交给框架
4.1 选 AgentCore 的考量:哪些活该外包
第一版跑通后我盘了一下,哪些代码是"业务逻辑",哪些是"Agent 运行时"。结果发现真正跟我的抓取摘要需求相关的代码不到三分之一,剩下全是消息管理、工具分发、循环控制这些通用逻辑。这些通用逻辑就是该外包的部分。
选 AgentCore 这类框架的核心判断标准是:它是否把"Agent 运行时"这层抽象得足够干净,同时又不剥夺你对关键环节的控制权。我关注三个点:工具注册是否声明式、上下文管理是否可干预、执行过程是否可观测。这三点决定了你用起来是省心还是闹心。
4.2 工具注册与上下文管理的框架化改造
AgentCore 把工具注册变成了装饰器风格,我只需要在函数上加个注解,框架自动读取函数签名和 docstring 生成工具定义。这一下省掉了我手写 JSON Schema 的活,而且 docstring 写得好,工具描述质量反而比手写更稳定。
@agent.tool def fetch_page(url: str) -> str: """当需要获取某个URL的正文内容时调用,输入必须是完整URL""" return do_fetch(url)上下文管理框架也接管了,我不用再手动维护消息数组。但这里有个关键点:框架托管不等于你撒手不管。我仍然需要配置上下文窗口策略,比如超过多少 token 要触发压缩、压缩时保留哪些关键信息。默认策略往往偏保守,容易把重要的中间结果压掉,导致 Agent"失忆"。我的做法是把工具调用的关键结果标记为"必须保留",其余的历史对话可以压缩。
4.3 框架带来的便利与新的约束
用框架最大的便利是开发速度。第一版三百多行的核心逻辑,第二版压缩到不到一百行,而且可读性更好。但代价也随之而来:框架的约定会渗透到你的代码结构里。比如它的工具返回值格式有要求,你的业务函数就得做适配;它的循环控制有默认行为,你想插入自定义的中断逻辑就得找钩子。
我遇到的一个具体约束是:框架默认的工具调用是串行的,但我的抓取任务其实可以并发。查了文档发现要开启并发需要额外配置,而且并发下的错误处理逻辑跟串行不一样。这提醒我一件事——用框架前一定要把它的执行模型搞清楚,别默认它跟你想的一样。
| 对比项 | 第一版手写 | 第二版 AgentCore |
|---|---|---|
| 核心代码行数 | 300+ | <100 |
| 工具定义方式 | 手写 JSON Schema | 装饰器自动生成 |
| 上下文管理 | 全手动 | 框架托管+策略配置 |
| 并发支持 | 自己实现 | 需配置开启 |
| 调试难度 | 低(全透明) | 中(部分黑盒) |
5. 第三版:做成 Claude Code 插件,让 Agent 融入工作流
5.1 为什么还要做第三版:从"程序"到"动作"
前两版都是独立程序,我得专门去运行它。但我日常大部分时间都在编辑器里,如果这个 Agent 能变成我敲一个命令就能触发的东西,使用频率会完全不一样。这就是第三版的动机:把 Agent 从一个需要主动运行的程序,变成一个随手可及的动作。
Claude Code 的插件机制正好满足这个需求。它允许你定义自定义命令,命令背后可以挂脚本、可以调模型、可以访问当前工作区的上下文。这比独立程序多了两个天然优势:一是不用切换窗口,二是我可以把当前正在看的文件、选中的代码作为输入传进去。
5.2 插件结构拆解与命令注册
Claude Code 插件的结构其实很轻。核心是一个配置文件加若干命令定义。命令定义里我指定命令名、描述、以及触发时执行什么。执行部分我选择调用一个本地脚本,脚本里再走我的 Agent 逻辑。
这里有个设计选择值得说:插件本身不做重逻辑,只做"入口和上下文收集",真正的 Agent 逻辑还是放在独立脚本里。这么设计的原因是插件运行环境相对受限,而且调试不如独立脚本方便。把重逻辑留在外面,插件只负责把工作区上下文传进去、把结果展示出来,职责清晰,也方便复用第二版的代码。
{ "commands": [ { "name": "digest", "description": "抓取并摘要当前关注的技术热帖", "script": "scripts/digest.sh" } ] }5.3 插件形态下的输入输出设计
插件形态最需要重新设计的是输入输出。独立程序里我可以随便打印日志,但插件里输出要克制,因为它是嵌在对话流里的。我的做法是:过程信息走日志文件,只把最终结果以简洁格式返回给对话。这样既不影响使用体验,出问题时又能去日志里查。
输入方面,我利用插件能拿到当前工作区路径这个特性,让 Agent 可以读取我当前项目里的配置文件来决定抓取哪些源。这就把"配置"和"使用场景"绑定了——我在做 A 项目时触发 digest,它抓的是 A 项目相关的源;切到 B 项目,抓的就是 B 的源。这种上下文感知是独立程序很难做到的。
注意:插件里调用外部脚本时,要注意工作目录的问题。宿主的工作目录和脚本期望的目录往往不一致,我踩过这个坑,脚本里所有相对路径都改成了基于脚本自身位置的绝对路径。
6. 三版横向对比:什么场景该选哪一版
6.1 开发效率、可控性、维护成本的三角权衡
做完三版,我对这个三角有了很具体的感受。开发效率上,第二版 AgentCore 最快,第一版最慢;可控性上,第一版最高,第三版受宿主约束最多;维护成本上,第一版随着工具增多线性上升,第二版平缓,第三版因为逻辑外置所以也不高。
这里有个反直觉的点:可控性最高不等于最适合生产。第一版虽然什么都能改,但每次加功能都要动核心循环,改多了容易引入 bug。第二版虽然有些黑盒,但核心循环稳定,我只需要关注业务函数,长期维护反而更省心。
6.2 我的选型建议:按使用频率和定制深度决定
如果这个 Agent 你要天天用、且逻辑相对固定,我推荐做成 Claude Code 插件,融入工作流收益最大。如果逻辑复杂、需要频繁调整、对性能和可控性要求高,直接调 API 手写更合适。如果是团队协作、要快速搭出可维护的业务 Agent,AgentCore 这类框架是甜点区。
还有一个维度是"这个 Agent 是给谁用的"。给自己用,插件形态最舒服;给别人用或者要部署成服务,独立程序更合适;要在团队内共享和迭代,框架化的版本更容易交接。
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 个人日常高频使用 | Claude Code 插件 | 融入工作流,触发成本低 |
| 深度定制、性能敏感 | 直接调 API | 完全可控,无框架开销 |
| 团队业务 Agent | AgentCore 框架 | 开发快,易维护,易交接 |
| 需要部署成服务 | 直接调 API 或框架 | 插件形态不适合独立部署 |
7. 实操中踩过的坑与排查技巧
7.1 工具调用参数错误的三种典型表现
第一种是参数名对不上。模型有时候会自作主张把url写成link,尤其是工具描述里用了"链接"这种词的时候。解决办法是在 description 里明确参数名,或者干脆在工具函数里做一层别名兼容。
第二种是参数类型错。比如要求传字符串,模型传了个对象。这种通常是因为 JSON Schema 定义不够严格,加上"type": "string"之后明显改善。
第三种是必填参数缺失。模型有时候会漏传,尤其是参数多的时候。我的做法是把参数控制在三个以内,超过就拆成多个工具,实测下来参数越少调用越准。
7.2 上下文超限与循环卡死的处理
上下文超限是长任务的头号杀手。我遇到过一次任务跑到一半报 token 超限,原因是工具返回的网页正文太长,几轮下来就把窗口撑爆了。解决办法是在工具层面做截断,抓取工具返回前先截到合理长度,同时把完整内容存到本地文件,只在上下文里放摘要和文件路径。这样模型需要细节时可以再调工具去读文件。
循环卡死我遇到过两次。一次是模型反复调用同一个工具拿同样的结果,另一次是模型在两个工具之间来回横跳。前者我在循环里加了"相同工具相同参数连续调用超过两次就强制中断"的检测,后者我加了总轮数上限。这两个保险丝后来救了我很多次。
7.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型不调用工具 | 工具描述不清 | 检查 description 是否说明使用时机 |
| 参数名错误 | 描述与 schema 不一致 | 统一参数命名,加别名兼容 |
| 上下文超限 | 工具返回过长 | 工具层截断,大内容存文件 |
| 循环卡死 | 无终止条件 | 加轮数上限和重复调用检测 |
| 插件找不到脚本 | 工作目录不一致 | 用基于脚本位置的绝对路径 |
| 框架并发异常 | 未开启并发配置 | 查框架文档确认执行模型 |
提示:这些坑里,上下文超限和循环卡死是最致命的,因为它们会让整个任务失败。建议在写第一版时就把这两个保险丝加上,别等出问题再补。
8. 一些关于 Agent 落地的个人体会
做完这三版,我最大的体会是:Agent 的难点从来不在模型本身,而在你如何组织它和外部世界的交互。模型能力再强,工具描述写得烂、上下文管理得乱、错误处理得糙,整个 Agent 就是不可用的。反过来,模型能力一般,但工程做扎实了,很多任务也能跑得挺稳。
另一个体会是关于"抽象层级"的选择。我见过太多人一上来就用最重的框架,结果出了问题完全不知道从哪查。也见过有人坚持全手写,结果维护成本高到项目烂尾。我的建议是:先用最裸的方式跑通一遍,理解每个环节在干什么,然后再根据实际需求往上加抽象。你亲手写过一遍 Agent 循环,再用任何框架都能一眼看穿它在帮你做什么、藏了什么。
最后分享一个我一直在用的小技巧:给 Agent 加一个"决策日志",把每一轮模型为什么调用这个工具、拿到结果后怎么想的,都记下来。这个日志在调试时价值极高,很多时候你看一眼日志就知道是模型理解错了,还是你的工具返回有问题。这个习惯帮我省下的排查时间,比我优化任何一处代码都多。