Agent系统的设计思路探讨

封面

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操作,二来有很多环节可能需要用户的介入确认,输入密码之类的敏感操作,聊天系统很难解决这类问题。

另外,大模型调用工具也存在一些问题:

  1. 每次工具调用结果都需要回传大模型,需要反复请求,效率很低,tokens却很费,
  2. 整个过程都由大模型来控制,但当上下文持续增长时有失控的风险。
  3. 由于上下文的限制,如果工具的调用结果很长,只能截断后再给大模型,容易缺失信息。

你可以试想一下这种场景:你的Agent告诉你他帮你选购了一个iPhone手机,打9折很便宜,给你发了几张iPhone的照片和价格,让你确认下单,你敢确认吗?如果它让你输入密码,你愿意在聊天框中输吗?

但如果,它是打开淘宝界面真实选购了一个iPhone手机,确实打9折,所有信息都像自己购物一样可以真实查看到,是不是就没这么多顾虑了?

让Agent自己跑起来

像这种操作具体要怎么设计呢?

大模型只负责理解用户的需求,输出一套Agent能识别的操作步骤,Agent自己主动运行相应的程序来执行这些操作步骤,由于是在原系统上执行,所有过程都会遵守原系统的设定,不会越权,不会将输入的密码提交给大模型,用户能看到操作过程,能理解操作结果。

范式转变

这一切当然也可以设计成工具让大模型来调用工具来实现,也可以干脆让大模型直接操作浏览器,但这些方案与让Agent主动运行程序来实现有着本质的区别:

工具调用是由大模型作主,大模型在这个场景里是主角,一切由它控制,在这个场景里工具调用的结果本身也会影响大模型的判断,如果是操作浏览器准确性也存在问题,看看层不出穷的浏览器操作工具就知道,谁都还没有做到最好。

而由Agent本身的程序来执行动作的方案就只需要大模型负责识别用户意图,生成操作步骤就够了,这一步才是大模型擅长的,后面的步骤都由程序负责,我们可以预先设计好哪些节点要人来确认,即使大模型生成的步骤里没有说明要确认也没关系,程序绝不会跳过,在这个场景里大模型不再是主角而是协助,程序也是协助,人才是主角。

程序的优势是确定性强,没有跑偏、丢三落四的毛病,执行效率也比大模型高,所以对于工作流这样的场景我们可以多利用程序来发挥它的优势,不必一切都依赖大模型。

如果你还没理解,我们可以用研发工作流程这个场景来进行说明:

  • 大模型理解用户需求
  • 大模型生成开发计划,比如拆分原子任务、验收标准等,总之是生成了10个任务。
  • 大模型决定逐个执行任务
  • 大模型收到任务执行完通知决定验收
  • 大模型发现验收不通过决定执行修复动作
  • 大模型最终判断所有任务都执行完了高兴地向用户汇报

整个过程都是大模型在决策,在控制,Agent只负责调用工具,没做其他的事情。

很多人应该都碰到过类似的问题:

  • 大模型执行了几个任务后突然停下来了。
  • 大模型执行过程中漏掉了一个任务。
  • 大模型把两个任务顺序搞反了等等。

为什么呢?因为大模型普遍存在上下文污染,跑偏,幻觉,注意力漂移等问题,确定性不是大模型的强项。

为此业界提出了很多SDD框架,harness工程方法论等,都是在尝试控制大模型的这种不够稳定的行为。

虽然很多方法都不错,但这些都只是围绕着控制大模型做文章,没有尝试改造一下Agent的程序设计,去利用程序的优势。

要知道如果想稳定地执行计划,程序才是最佳的选择,因为程序没有自己的心思,它只会按事先定义好的规则来执行。

大模型擅长的是语义识别和推理,而程序擅长的是确定性,如果能让两者互补岂不是更好?

DSL:让大模型和程序对话

怎么互补呢?

我们需要为大模型与Agent程序制定一套协议,大模型的输出必须符合协议规定的格式,这样程序才能正确识别,自然语言是不行的。

还是上面那个例子,大模型识别用户意图分析完用户需求后,将设计出来的任务列表以JSON格式输出,程序是可以解析JSON的,这样就能提取出这些任务,按照任务的依赖关系进行编排执行,是串行还是并行?执行失败应该跳到哪个节点等等程序都能自己判断,不需要大模型来决策,程序执行的确定性让人完全不担心会跑偏。

在执行过程中每个具体的任务要怎么执行呢?还是提交给大模型,仍然由大模型调用各类工具来完成,只是每个任务都是一个单独的会话,由一个相应的Agent来执行,这样不但能并行,还做到了天然的上下文隔离。

这套协议,本质上就是一套DSL,在不同的领域可以设计不同的协议格式,设计专用的Agent来执行任务。

DSL工作流程

比如上面说的编排任务的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能包含Agent吗?

上面提到的subagent算不算一种包含关系呢?算,但还不够。

在程序语言设计里,当我们设计一个结构体只能包含数字、字符等基本数据类型的时候,它的表达能力会很受限,仅仅能做一些很简单的事。

但当我们允许一个结构包含其他结构体的时候,它就爆发出了无限的潜力,可以编写出非常复杂的程序。

Agent包含关系

所以,要发挥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可能是不必要的。它的用途可以有更好的方案,更简单的方式来承载。

◆ ◆ ◆

核心观点:大模型擅长语义识别和推理,程序擅长确定性执行。让它们各司其职,而不是让大模型既当大脑又当手脚。