首页 / 资讯中心 / 文章详情

MIT JAZ 研究:Harness as a Language,用声明式编排解决 Agent 工程化难题

MIT JAZ 研究:Harness as a Language,用声明式编排解决 Agent 工程化难题 ★ FEATURED ARTICLE
1. 从写提示词到造工具JAZ 到底在解决什么问题大多数人接触 Agent 开发第一反应是去调 API、拼 prompt、接几个工具函数然后发现一个尴尬的事实模型能力在涨但你的 Agent 还是那副样子——换个任务就得重写一遍流程加个新工具就得改一遍代码想复用上次跑通的逻辑只能靠复制粘贴。这不是模型的问题是编排层的问题。MIT 这项新研究提出的 JAZ核心主张就一句话Harness as a Language。翻译成人话就是——把驾驭 Agent 的那套东西从代码里抽出来变成一门语言。你不再是用 Python 写一堆 if-else 去控制 Agent 该调哪个工具、该走哪条分支而是用一种声明式的语法去描述这个 Agent 在什么条件下做什么。这里要先厘清一个被热词搅浑的概念Harness 和 Agent 到底啥区别。Agent 是干活的主体它包含模型、记忆、工具集Harness 是套在 Agent 外面的那层壳负责调度、约束、编排、观测。打个比方Agent 是司机Harness 是方向盘加导航加行车记录仪。很多人把两者混为一谈结果写出来的代码既当司机又当方向盘耦合得一塌糊涂改一处崩三处。JAZ 想干的事就是让 Harness 这层壳拥有自己的表达方式。它引入了几个关键概念Primitive原语、invoke调用、Harness 工程。原语是最小的可组合单元比如读取一个文件调用一次模型判断一个条件invoke 是把原语串起来的动作而 Harness 工程就是把这些原语和调用组织成一套可维护、可复用、可测试的编排逻辑。为什么这件事值得单独拿出来讲因为现在 Agent 开发的痛点几乎全集中在编排层。你去看那些真实落地的 Agent 项目模型调用本身往往只占代码量的两成剩下八成全是状态管理、错误重试、工具路由、上下文裁剪、并发控制。这些活儿琐碎、重复、容易出错而且每换一个业务场景就得重来一遍。JAZ 的价值就在于它试图给这八成脏活累活提供一套统一的、声明式的表达方式。适合谁来读这篇如果你正在做 Agent 开发被编排逻辑折磨过如果你在评估 Agent 框架想知道声明式编排这条路走不走得通如果你只是好奇 MIT 又在搞什么新花样——都能从下面这些拆解里拿到东西。我会尽量把 JAZ 的设计思路、它和现有方案的区别、以及如果你要上手该怎么落地讲得能直接抄作业。2. Harness as a Language 的设计哲学为什么是语言而不是框架2.1 框架的困境你永远在填它的坑先说说为什么现有的 Agent 框架让人又爱又恨。主流的 Agent 框架无论是偏编排的还是偏工具集的本质上都是命令式的——你得用宿主语言Python、TypeScript去写控制流。框架给你提供了一堆抽象类、装饰器、回调钩子你按照它的规矩把逻辑填进去。问题出在填这个动作上。框架的抽象层次是固定的但你的业务需求是流动的。框架假设你会按 A→B→C 的顺序走结果你的场景需要 A→C→B 或者 A→(B|C)框架假设工具调用是同步的结果你遇到一个必须异步等待的外部服务框架假设错误处理就是重试三次结果你需要根据错误类型走完全不同的补偿路径。每一次这种框架假设和实际需求的错位你都得用宿主语言的代码去绕、去 hack、去写适配层。绕到最后你的项目里框架代码和业务代码搅在一起谁也说不清哪行是框架的规矩、哪行是你的补丁。想升级框架版本先祈祷它没改那些你 hack 过的内部接口。想把编排逻辑复用到另一个项目对不起它和这个项目的业务代码焊死了。2.2 语言的优势表达力与可组合性JAZ 选择语言这条路本质上是想解决表达力和可组合性这两个框架解决不好的问题。语言的第一优势是声明式。你描述要什么而不是怎么做。比如你要表达先尝试用工具 A 处理失败了再用工具 B 兜底两次都失败就上报在命令式框架里你得写 try-catch 嵌套加状态标记在 JAZ 这类语言里这可能就是一行带 fallback 语义的声明。声明式的好处是编排逻辑变得可读、可审查、可 diff——你一眼能看出这个 Agent 的行为边界在哪。语言的第二优势是可组合。原语Primitive是最小单元原语可以组合成更大的原语大原语再组合成完整的 Harness。这种组合是结构化的不是靠函数调用堆出来的。结构化组合意味着你可以对编排逻辑本身做静态分析——比如检查有没有死循环、有没有不可达分支、有没有资源泄漏。这是命令式代码很难做到的因为命令式代码的控制流是运行时才确定的。语言的第三优势是可移植。编排逻辑一旦从宿主语言里抽出来它就不再绑定某个具体的运行时。同一套 Harness 描述理论上可以编译到不同的执行引擎上——今天跑在本地明天跑在分布式环境后天换个模型供应商编排层不用动。这对那些需要Agent anywhere到处部署的场景特别有价值。2.3 原语设计Primitive 的粒度怎么定JAZ 里最值得琢磨的设计是Primitive 的粒度。粒度太细比如把加一都做成原语那编排逻辑会碎成一地写起来比汇编还累粒度太粗比如把完成一次完整的客服对话做成一个原语那又失去了组合的灵活性等于把框架的坑换了个地方埋。合理的粒度应该落在语义边界上。什么叫语义边界就是这个操作有明确的输入输出契约有独立的错误语义有可观测的执行痕迹。比如调用一次模型是一个原语因为它有明确的输入prompt 参数和输出文本 token 消耗失败模式清晰超时、限流、内容过滤。而处理用户投诉不是一个原语因为它内部包含了太多可变逻辑应该由多个原语组合而成。我个人的经验是原语的数量控制在十几个到几十个之间比较舒服。太少了不够用太多了记不住。JAZ 目前暴露的原语集从热词里能看出端倪——invoke 是核心动作原语围绕它还有条件判断、循环、并发、错误处理、状态读写这几类。这个划分和大多数编排语言的设计是吻合的说明它踩在了合理的工程经验上。2.4 和现有方案的对比JAZ 站在哪个位置把 JAZ 放到现有的 Agent 编排方案光谱里看会更清楚它的定位。方案类型代表思路编排方式主要痛点纯代码编排手写 Python/TS 控制流命令式耦合高、难复用、难分析框架编排提供抽象类和钩子半声明式框架假设与需求错位图编排用 DAG 描述流程声明式表达力受限难处理动态分支语言编排JAZ专用语法描述 Harness声明式 可组合学习成本、生态成熟度图编排DAG是前几年比较流行的思路用节点和边描述流程。它的优点是直观缺点是表达力天花板低——真实 Agent 的行为往往不是静态图能描述的动态分支、递归、条件循环这些用图表达起来很别扭。JAZ 走语言路线理论上表达力上限更高代价是需要定义语法和语义学习曲线更陡。提示选型时别只看表达力上限还要看团队的学习成本和调试体验。语言类方案在可读性和可分析性上有优势但调试工具链如果跟不上排查问题会比命令式代码更痛苦。3. invoke 与 PrimitiveJAZ 的执行模型拆解3.1 invoke 的语义一次调用到底发生了什么invoke是 JAZ 里最高频的动作原语理解它的语义是理解整个执行模型的关键。一次 invoke 表面上是调用某个东西但底下至少包含四件事参数求值、目标解析、执行、结果处理。参数求值阶段JAZ 需要确定这次调用传什么参数。参数可能来自字面量、来自上游原语的输出、来自外部状态、来自环境变量。这里有个容易踩的坑参数的求值时机。是调用前一次性求值还是执行时才惰性求值如果是惰性求值那参数表达式里引用的状态在执行时可能已经变了。JAZ 的设计倾向于显式求值也就是在 invoke 之前把参数算清楚这样行为可预测调试也容易。目标解析阶段JAZ 要确定调用谁。这个谁可能是一个工具、一个子 Harness、一个模型端点。解析过程可能涉及路由逻辑——比如根据输入类型选择不同的工具。路由逻辑本身也是编排的一部分JAZ 把它做成可声明的而不是藏在代码里。执行阶段就是真正干活。这里的关键是执行语义同步还是异步阻塞还是非阻塞超时怎么算JAZ 作为编排语言需要把这些语义显式化而不是依赖宿主语言的默认行为。比如一个 invoke 默认是同步阻塞的但你可以标注它为异步让它返回一个 future 之类的句柄后续再 await。结果处理阶段决定这次调用的输出怎么进入下一步。是直接透传还是做转换还是只取其中一部分JAZ 通常提供模式匹配或字段提取的语法让你精确控制结果的流向。3.2 Primitive 的组合规则怎么把小块拼成大块原语单独用没意义价值在组合。JAZ 的组合规则从设计上看遵循几条原则。顺序组合是最基本的前一个的输出喂给后一个的输入。但顺序组合有个隐含假设数据流是线性的。真实场景里经常需要扇出一个结果喂给多个下游和扇入多个结果汇聚成一个。JAZ 需要提供对应的组合子来处理这些拓扑。条件组合处理分支。if-else 是最简单的但 Agent 场景里更常见的是守卫guard——满足条件才执行不满足就跳过或走默认路径。守卫比 if-else 更声明式因为它把条件和动作解耦了条件可以独立测试和复用。循环组合处理重复。Agent 里最常见的循环是反复尝试直到成功或达到上限也就是重试循环。JAZ 如果把重试做成原语级的组合子而不是让你手写 while那编排逻辑会干净很多。重试的语义细节很多退避策略、最大次数、哪些错误可重试、重试时状态怎么重置——这些都应该在组合子层面统一处理而不是每个地方各写各的。并发组合处理并行。多个独立的 invoke 可以并发执行然后等所有结果。并发组合的难点在于错误处理——一个失败了其他要不要取消部分成功怎么算JAZ 需要定义清晰的并发语义否则用起来会踩坑。3.3 状态管理Harness 的记忆放在哪Agent 有记忆Harness 也得有状态。JAZ 里的状态管理核心问题是状态存在哪、谁能读写、生命周期多长。从粒度上分状态至少有三个层次单次 invoke 的局部状态比如这次调用的中间结果、单次 Harness 执行的会话状态比如这次任务已经走了哪些步骤、跨执行的持久状态比如用户偏好、历史记录。JAZ 需要为不同层次的状态提供不同的声明方式而不是一股脑塞进一个全局字典。局部状态最简单作用域限于当前原语执行完就丢。会话状态复杂一些它需要在原语之间传递但执行结束就清空。持久状态最麻烦它涉及存储、序列化、并发访问、版本迁移。很多 Agent 项目在持久状态上翻车就是因为早期图省事用了全局变量后期想改成持久化发现到处都在读写改不动。JAZ 的设计如果够严谨应该把状态的读写权限也纳入编排描述。哪些原语能读、哪些能写、写的时候要不要加锁——这些在并发场景下是必须显式声明的否则会出现竞态。这一点在AI Agent 怎么扛并发这个热词里体现得很明显说明大家已经被并发状态问题坑过了。3.4 错误语义失败不是异常是流程的一部分命令式代码里错误处理靠异常。异常的问题是它打断了控制流你得用 try-catch 把控制流接回来接得不好就漏了或者吞了。JAZ 作为编排语言更合理的做法是把失败建模成一等公民——失败是一种正常的结果和成功并列编排逻辑显式地处理这两种结果。这意味着 invoke 的返回不是一个值而是一个结果类型可能是成功值也可能是失败原因。后续的组合子根据结果类型决定走哪条路。这种建模方式的好处是失败路径和成功路径一样可读、可测试、可组合。你可以在编排层面统一决定失败三次就降级特定错误就换工具所有失败都记录到审计日志而不是在每个调用点重复写错误处理。注意把失败建模成结果类型会让编排逻辑变长因为每个 invoke 后面都得处理两种结果。JAZ 如果提供默认的失败传播语义类似 Rust 的?操作符能大幅减少样板代码。选型时要看它有没有这类语法糖。4. 从 JAZ 看 Harness 工程编排层该怎么工程化4.1 Harness 工程和普通开发的区别Harness 工程这个词最近被提得很多但它和普通软件开发到底差在哪很多人说不清。我的理解是Harness 工程的核心挑战在于它编排的是不确定的东西。普通软件开发你调用的函数是确定的——给同样的输入出同样的输出失败模式也是有限的几种。Harness 编排的是模型调用和工具调用这两者都不确定。模型可能这次答对下次答错工具可能这次通下次超时。这种不确定性让 Harness 工程必须把应对不确定性作为一等目标而不是事后补丁。具体到工程实践上这意味着几件事。可观测性必须内建不能事后加。你得知道每次执行走了哪条路径、每个原语的耗时、每次模型调用的 token 消耗、每次失败的具体原因。没有这些你根本没法优化。可测试性必须设计进去不能靠手动跑。编排逻辑要能在不调用真实模型的情况下测试否则测试成本高到没人愿意写。可演进性必须考虑因为模型和工具都在快速变化编排逻辑得能跟着变而不推倒重来。4.2 可观测性编排逻辑的行车记录仪JAZ 这类语言在可观测性上有个天然优势编排逻辑是结构化的所以执行轨迹也是结构化的。命令式代码的执行轨迹是一堆函数调用栈你得靠日志拼凑出发生了什么结构化编排的执行轨迹是一棵树或者一张图每个节点对应一个原语每条边对应一次数据流。这种结构化的轨迹可以直接拿来做可视化、做分析、做告警。具体要观测什么至少这几样路径这次执行走了哪些原语、耗时每个原语花了多久瓶颈在哪、数据每个原语的输入输出用于复现问题、异常哪些原语失败了失败原因是什么。这四样凑齐基本能覆盖大部分排查场景。有个实操心得观测数据的采样策略要早定。全量记录数据量太大尤其是模型调用的输入输出可能很长。但采样太狠又会漏掉关键问题。我的做法是分层采样——路径和耗时全量记录数据量小数据按比例采样比如 10%异常全量记录。这样既能看全局又能查个案。4.3 可测试性怎么在不烧钱的情况下测编排Agent 测试最烧钱的地方是模型调用。每次测试都真调模型成本高、速度慢、结果还不稳定。JAZ 这类编排语言如果设计得当可以让编排逻辑和模型调用解耦从而支持 mock 测试。解耦的关键是把模型调用也当成原语。既然它是原语那就可以替换——测试时用一个 mock 原语替换真实原语返回预设的结果。这样你测的是编排逻辑本身条件分支走对没有、重试逻辑触发没有、状态传递正确没有。这些逻辑问题用 mock 就能测出来不需要真调模型。编排逻辑的测试还应该覆盖边界情况模型返回空结果怎么办、工具超时怎么办、并发时状态冲突怎么办。这些情况在真实环境里偶发但一旦发生就是事故。用 mock 可以稳定复现这些边界把它们变成常规测试用例。4.4 版本管理编排逻辑的演进策略编排逻辑会变而且会经常变。模型升级了要调 prompt工具换了要改路由业务需求变了要加分支。这些变化如果管理不好会变成技术债。JAZ 作为语言在版本管理上有优势编排逻辑是文本文本就能 diff、能 review、能回滚。这比命令式代码里散落各处的逻辑好管理得多。但光有 diff 还不够还需要语义化的版本管理——不只是看文本变了什么还要看行为变了什么。实操上我建议给编排逻辑建立变更影响分析的习惯。改一个原语要评估它影响哪些下游改一个条件要评估它改变了哪些路径。这种分析在结构化编排里是可行的因为依赖关系是显式的。命令式代码里做这种分析基本靠人肉读代码容易漏。5. 落地 JAZ 思路的实操路径从概念到能跑5.1 环境准备与最小可跑示例假设你想在自己的项目里落地 JAZ 的思路第一步不是去装什么工具而是先把编排逻辑从业务代码里抽出来。这一步和具体用什么语言无关是思维方式的转变。具体做法找一个你现有的 Agent 流程把它画成图。节点是原语模型调用、工具调用、条件判断、状态读写边是数据流。画的过程中你会发现很多逻辑其实是重复的——同样的重试、同样的错误处理、同样的状态读写。这些重复就是可以抽象成原语和组合子的地方。抽出来之后用伪代码或者简单的 DSL 描述这个流程。不用一上来就追求完整的语言实现先用最朴素的方式把声明式编排的感觉找到。比如你可以用 YAML 或者 JSON 描述流程然后写一个解释器去执行它。这个解释器不需要很强大能跑通基本流程就行。# 一个极简的编排描述示例 harness: name: customer_query steps: - id: classify invoke: model.classify input: {{ user_input }} output: category - id: route switch: {{ category }} cases: refund: - invoke: tool.refund_lookup input: {{ user_input }} technical: - invoke: tool.kb_search input: {{ user_input }} default: - invoke: model.general_reply input: {{ user_input }}这个例子里invoke是原语switch是组合子{{ }}是参数引用。整个描述是声明式的读起来就是先分类再根据分类走不同路径。这种描述可以直接被解释器执行也可以被静态分析。5.2 原语库的搭建顺序落地 JAZ 思路原语库的搭建顺序很关键。我的建议是从少到多按需添加而不是一开始就设计一套大而全的原语。第一批原语只需要覆盖最核心的动作模型调用、工具调用、条件判断、状态读写。这四个能支撑起大部分简单流程。先用这四个跑通一个真实场景感受一下声明式编排的优缺点。第二批原语根据实际痛点添加。常见的痛点包括重试、超时控制、并发、结果转换、日志记录。这些如果每次都手写会很烦做成原语或组合子能大幅简化。第三批原语是那些特定领域的。比如做 RPA 落地可能需要操作浏览器读取表格发送通知这类原语做数据处理可能需要读取文件转换格式写入数据库。这些原语和业务强相关不适合放进通用库应该按项目单独维护。提示原语库的维护成本不低每个原语都要有清晰的契约、错误语义、测试用例。别为了看起来完整而堆原语用不上的原语就是负债。5.3 和现有 Agent 框架的集成方式你大概率不是从零开始而是已经用了某个 Agent 框架。JAZ 的思路可以和现有框架集成不一定非要推倒重来。集成方式有两种。一种是包裹式把现有框架的调用封装成原语编排层用 JAZ 思路描述执行时调用框架。这样编排逻辑和框架实现解耦将来换框架只改原语实现编排描述不动。另一种是并存式新流程用 JAZ 思路写老流程保持原样逐步迁移。这种方式风险小但会有两套逻辑并存维护成本高。我倾向于包裹式但前提是现有框架的调用接口足够清晰能干净地封装成原语。如果框架的调用逻辑和业务代码搅在一起封装起来会很痛苦这时候可能得先做一轮重构。5.4 并发场景下的编排注意事项AI Agent 怎么扛并发是个高频问题JAZ 这类编排语言在并发上有天然优势但也有坑。优势在于并发逻辑可以声明式表达。哪些原语可以并行、并行度多少、失败怎么处理这些都能在编排层统一描述而不是散落在代码里。声明式并发的好处是可分析——你能静态看出哪些地方有并发、并发之间有没有共享状态、有没有死锁风险。坑在于共享状态。并发执行时多个原语可能同时读写同一个状态产生竞态。JAZ 需要在编排层显式声明状态的访问模式比如这个状态只读这个状态需要独占写。如果语言层面不提供这种声明你就得靠约定而约定在并发场景下很容易被打破。另一个坑是资源限制。并发度不是越高越好模型调用有速率限制工具调用有连接池限制。编排层需要感知这些限制做背压控制。JAZ 如果提供资源声明和背压原语能省很多事。6. 我对 JAZ 这类方案的判断和实操建议6.1 它适合什么样的团队和场景JAZ 这类Harness as a Language的方案不是万能药。它适合的场景有明确特征编排逻辑复杂且多变、需要多人协作维护、对可观测性和可测试性有要求。如果你的 Agent 就是简单的一问一答或者流程固定到几乎不会变那用 JAZ 是杀鸡用牛刀直接写代码更快。团队方面它适合有一定工程素养的团队。声明式编排、原语设计、组合子这些概念需要团队成员理解并达成共识。如果团队里大部分人对编排没有概念上来就用 JAZ可能会水土不服——他们会觉得还不如直接写代码直观。场景方面它特别适合需要长期演进的 Agent 项目。一次性脚本怎么写都行但一个要跑几年、要不断加功能、要多人维护的 Agent编排层的工程质量就是生死线。JAZ 这类方案的价值在长期项目里才能充分体现。6.2 上手时最容易踩的三个坑第一个坑是过度抽象。刚接触声明式编排容易兴奋想把所有东西都抽象成原语。结果原语库膨胀到几十上百个每个原语用不了几次维护成本却很高。我的建议是原语只在被复用三次以上时才抽象否则就内联在编排逻辑里。第二个坑是忽视调试体验。声明式编排的可读性好但调试体验往往不如命令式代码。命令式代码可以打断点、单步执行声明式编排如果工具链跟不上排查问题只能靠日志。上手前一定要确认调试工具链是否够用否则遇到问题会很痛苦。第三个坑是状态管理失控。前面提过状态是编排层最容易出问题的地方。上手时就要建立状态管理的规范哪些状态放哪一层、谁能读写、生命周期多长。别等到状态乱成一锅粥再回头治理那时候改造成本高得吓人。6.3 后续可以怎么扩展JAZ 的思路可以往几个方向扩展。一是编排逻辑的静态分析既然编排是结构化的就可以做类型检查、死代码检测、资源泄漏检测。这些在命令式代码里要靠复杂工具在结构化编排里可以做得更轻量。二是编排逻辑的可视化结构化的编排天然适合画成图可视化编辑器能降低上手门槛。三是编排逻辑的自动优化比如自动识别可并行的部分、自动插入重试和降级、自动做资源调度。这些扩展方向有些 JAZ 可能已经在做有些还需要生态慢慢补。作为使用者我的建议是先关注核心价值——声明式编排带来的可读性、可组合性、可分析性。这些是 JAZ 的立身之本其他都是锦上添花。最后分享一个我自己的体会编排层的工程质量往往决定了 Agent 项目能走多远。模型能力会涨工具会换但编排逻辑是那个把一切串起来的东西。把它当成一等公民来对待用合适的语言和工具去表达它长期看是值得的投入。JAZ 提供了一个值得参考的方向至于要不要全盘采用得看你的具体场景和团队情况——但至少它让你意识到Harness 本身值得被认真设计这件事这已经很有价值了。
阅读完成 · 觉得有帮助?
咨询建站