Agent系统的设计思路探讨
Agent的架构大体上是这样的:
用户输入需求,大模型利用推理能力输出结果要求调用工具,Agent替大模型调用工具,将结果返回给大模型,大模型决定下一步的操作,当大模型决定不需要调用工具时输出一段结果给Agent,Agent判断当前轮次结束,展示大模型输出结果后等待用户再次输入。
Agent的设计目的是替大模型调用工具,这也是Agent这个词的本来含义:代理。Claude Code是这样设计的,OpenCode、Cursor等有名的编程Agent都是这个架构。
所以很多人说:Agent的架构一点也不复杂,里面也没有多大技术含量。
话说的虽然没错,但Agent的架构只能有这一种吗?可以有其它的设计方式吗?
Chat Runner的局限
大模型调用完工具后输出结果就会停下来等待用户输入,这本质上是AI聊天工具的设计逻辑,是一种chat runner。
这个流程的特点是:在人-Agent-大模型这三者中,只有人和大模型有"主动行为",Agent没有,在聊天的场景里它也确实不需要。
但Agent面临的场景却远不止于聊天。
比如这个场景:有一个复杂的系统功能繁多很难操作,用户经常搞不清楚所需要的功能到底藏在哪里,需要我们在系统里开发一个Agent来协助用户。
按照聊天工具的设计思路,我们将系统的操作知识和用户的输入提交给大模型,让大模型指导用户怎么操作。如果要更进步一点,可以在系统上再开发一套CLI,这样大模型就可以通过工具调用来替用户操作了。
但是,一套复杂系统往往很难将所有功能都事无巨细提供相应的CLI操作,二来有很多环节可能需要用户的介入确认,输入密码之类的敏感操作,聊天系统很难解决这类问题。
另外,大模型调用工具也存在一些问题:
- 每次工具调用结果都需要回传大模型,需要反复请求,效率很低,tokens却很费,
- 整个过程都由大模型来控制,但当上下文持续增长时有失控的风险。
- 由于上下文的限制,如果工具的调用结果很长,只能截断后再给大模型,容易缺失信息。
你可以试想一下这种场景:你的Agent告诉你他帮你选购了一个iPhone手机,打9折很便宜,给你发了几张iPhone的照片和价格,让你确认下单,你敢确认吗?如果它让你输入密码,你愿意在聊天框中输吗?
但如果,它是打开淘宝界面真实选购了一个iPhone手机,确实打9折,所有信息都像自己购物一样可以真实查看到,是不是就没这么多顾虑了?
让Agent自己跑起来
像这种操作具体要怎么设计呢?
大模型只负责理解用户的需求,输出一套Agent能识别的操作步骤,Agent自己主动运行相应的程序来执行这些操作步骤,由于是在原系统上执行,所有过程都会遵守原系统的设定,不会越权,不会将输入的密码提交给大模型,用户能看到操作过程,能理解操作结果。
这一切当然也可以设计成工具让大模型来调用工具来实现,也可以干脆让大模型直接操作浏览器,但这些方案与让Agent主动运行程序来实现有着本质的区别:
工具调用是由大模型作主,大模型在这个场景里是主角,一切由它控制,在这个场景里工具调用的结果本身也会影响大模型的判断,如果是操作浏览器准确性也存在问题,看看层不出穷的浏览器操作工具就知道,谁都还没有做到最好。
而由Agent本身的程序来执行动作的方案就只需要大模型负责识别用户意图,生成操作步骤就够了,这一步才是大模型擅长的,后面的步骤都由程序负责,我们可以预先设计好哪些节点要人来确认,即使大模型生成的步骤里没有说明要确认也没关系,程序绝不会跳过,在这个场景里大模型不再是主角而是协助,程序也是协助,人才是主角。
程序的优势是确定性强,没有跑偏、丢三落四的毛病,执行效率也比大模型高,所以对于工作流这样的场景我们可以多利用程序来发挥它的优势,不必一切都依赖大模型。
如果你还没理解,我们可以用研发工作流程这个场景来进行说明:
- 大模型理解用户需求
- 大模型生成开发计划,比如拆分原子任务、验收标准等,总之是生成了10个任务。
- 大模型决定逐个执行任务
- 大模型收到任务执行完通知决定验收
- 大模型发现验收不通过决定执行修复动作
- 大模型最终判断所有任务都执行完了高兴地向用户汇报
整个过程都是大模型在决策,在控制,Agent只负责调用工具,没做其他的事情。
很多人应该都碰到过类似的问题:
- 大模型执行了几个任务后突然停下来了。
- 大模型执行过程中漏掉了一个任务。
- 大模型把两个任务顺序搞反了等等。
为什么呢?因为大模型普遍存在上下文污染,跑偏,幻觉,注意力漂移等问题,确定性不是大模型的强项。
为此业界提出了很多SDD框架,harness工程方法论等,都是在尝试控制大模型的这种不够稳定的行为。
虽然很多方法都不错,但这些都只是围绕着控制大模型做文章,没有尝试改造一下Agent的程序设计,去利用程序的优势。
要知道如果想稳定地执行计划,程序才是最佳的选择,因为程序没有自己的心思,它只会按事先定义好的规则来执行。
大模型擅长的是语义识别和推理,而程序擅长的是确定性,如果能让两者互补岂不是更好?
DSL:让大模型和程序对话
怎么互补呢?
我们需要为大模型与Agent程序制定一套协议,大模型的输出必须符合协议规定的格式,这样程序才能正确识别,自然语言是不行的。
还是上面那个例子,大模型识别用户意图分析完用户需求后,将设计出来的任务列表以JSON格式输出,程序是可以解析JSON的,这样就能提取出这些任务,按照任务的依赖关系进行编排执行,是串行还是并行?执行失败应该跳到哪个节点等等程序都能自己判断,不需要大模型来决策,程序执行的确定性让人完全不担心会跑偏。
在执行过程中每个具体的任务要怎么执行呢?还是提交给大模型,仍然由大模型调用各类工具来完成,只是每个任务都是一个单独的会话,由一个相应的Agent来执行,这样不但能并行,还做到了天然的上下文隔离。
这套协议,本质上就是一套DSL,在不同的领域可以设计不同的协议格式,设计专用的Agent来执行任务。
比如上面说的编排任务的DSL:
{
"id": "wf_20260517_103012_a8f3",
"schema": 1,
"goal": "Fix SDK generation and verify the result.",
"status": "running",
"nodes": [
{
"id": "research",
"type": "research",
"title": "Inspect SDK generation flow",
"depends_on": []
},
{
"id": "implement",
"type": "implementation",
"title": "Apply the code change",
"depends_on": ["research"]
},
{
"id": "test",
"type": "test",
"title": "Verify the change",
"depends_on": ["implement"]
},
{
"id": "review",
"type": "review",
"title": "Review implementation",
"depends_on": ["test"]
}
],
"policies": {
"on_node_failed": "ask_decision",
"max_attempts": 2
}
}
这一类Agent不再是chat runner,而是workflow runner,你也可以设计其他类型的runner,思路不必局限于聊天工具。
适合用DSL的场景也还很多,比如低代码领域:低代码系统本身就有一套DSL,可以基于此设计一套使用低代码的Agent工具来协助用户进行开发。
总之,在需要确定性的场景,我们可以让大模型来负责语义识别和推理,让Agent程序来负责确定性,而不用在大模型的缺点上跟大模型死磕。
重新定义Agent
首先我们来定义一下什么是Agent。
很多使用大模型来编排的工作流设计方案里都会用到 subagent来达到上下文隔离的效果,subagent也是另一个值得讨论的话题。
有人用操作系统调度进程来类比多个Agent的协作,将Agent类比成操作系统中的进程,看起来好像很有道理,但实际上混淆了Agent和会话的概念。
Agent是一个模板,我们每次创建的都是它的实例,也就是会话。
比如,同一个Agent我们可以开多个窗口分别启动,这时就是创建了多个会话。
会话才是应该类比于进程的概念,而Agent是进程所对应的程序,它定义了会话的行为规范。
这个混淆Claude Code得负一定责任,Claude Code总是把在主会话中新起一个会话称之为创建subagent,大家习惯了之后就把Agent和会话给混为一谈了。
Agent应该有什么样的结构呢?可以这样:
- 身份定义:它是谁,有什么知识背景?
- 行为规则:它应该遵守什么规则约束,工作流是怎样的?
- 可用工具集合:它能调用什么工具?
- 权限范围:它能操作哪些资源?
身份定义和行为规则不都是系统提示词吗?为什么拆成两部分?是的,确实都是系统提示词,但我们这里要定义的是一个Agent的抽象概念,而不是定义Agent实际要怎么实现。
把它定义成多个部分后,每个部分都可以单独设计,单独迭代,这样就方便多了。
并且,后面要添加新的部分也简单,比如最近很火热的Agent的记忆,也适合将它添加作为Agent的一部分,不需要考虑重写整个提示词。
Agent本身的程序执行逻辑算不算它的组成部分?其实也应该算,不同的执行逻辑可以实现不同类型的Agent,比单纯定义系统提示词要灵活很多。
Agent能包含Agent吗?
上面提到的subagent算不算一种包含关系呢?算,但还不够。
在程序语言设计里,当我们设计一个结构体只能包含数字、字符等基本数据类型的时候,它的表达能力会很受限,仅仅能做一些很简单的事。
但当我们允许一个结构包含其他结构体的时候,它就爆发出了无限的潜力,可以编写出非常复杂的程序。
所以,要发挥Agent的威力,我们也应该要允许Agent包含,并且不能像subagent那样仅仅只允许包含一层。
另外,一个Agent能做什么不能做什么应该由Agent本身的定义来决定,也就是由它定义的工具和权限来决定,不能像现在的subagent一样受到很多特殊限制。
所有的Agent都平等,只有分工的不同,它们都有幸福的未来。
Claude Code设计出subagent又要限制它的能力带了个坏头。
对Claude Code的反思
说到Claude Code,当它的源码泄露的时候,很多人说这下大家都可以向Agent界的天花板学习了,很多Agent工具又有优秀的设计可以抄了。
但从我们上面的讨论看,Claude Code并没有多少神奇的地方,它发明的SubAgent概念反而限制了Agent的威力,它本质上也只是一个chat runner。
一个好的Agent体系应该允许自由创建Agent,允许自由生成子会话,应该充分利用好Agent程序本身的能力,找到大模型与程序与人三者之间交互的平衡点。
下一步:Skill需要存在吗?
有人可能会说,Skill就是Claude Code发明的,它还是很优秀的,前面在讨论Agent的设计时怎么就没有讨论Skill呢?
是的,Skill这个概念很火,现在的Skill满天飞,几乎没有人不用它,编排任务用Skill,执行任务用Skill,测试用Skill,修复用Skill,几乎所有活都用Skill在干了。
但很少有人停下来反思:这个概念真的合理吗?它设计得真的足够好吗?
我的观点是:在一个Agent自由的王国里,Skill可能是不必要的。它的用途可以有更好的方案,更简单的方式来承载。
核心观点:大模型擅长语义识别和推理,程序擅长确定性执行。让它们各司其职,而不是让大模型既当大脑又当手脚。