AI Data Agent架构选型:通用Runtime+Plugin还是Agent Framework?
1. 从一个真实的技术选型困境说起去年下半年我接手了一个 AI Data Agent 的项目。需求很明确让用户用自然语言问数据相关的问题Agent 自动完成意图理解、SQL 生成、数据查询、结果解读这一整条链路。听起来是个典型的 Agent 应用场景但真正动手时第一个卡住我的问题不是模型选型也不是 Prompt 调优而是一个更底层的问题——我到底该用通用 Agent Runtime 加 Plugin 的方式搭还是直接上一个 Agent Framework这个问题看起来像是架构偏好之争但实际做下来我发现它直接决定了后面三个月的开发节奏、调试难度和上线后的可维护性。选错了不是不能跑而是每一步都别扭。我先把这两个概念说清楚因为很多刚接触 Agent 开发的朋友容易把它们混为一谈。通用 Agent Runtime Plugin的思路是有一个已经跑通的、负责 Agent 主循环的运行时Runtime它管的是思考—行动—观察这个循环本身包括和模型的对话管理、工具调用的分发、上下文的维护。而具体的能力比如查数据库、画图表、调 API全部以 Plugin 的形式挂载进去。Runtime 是稳定的、通用的Plugin 是变化的、业务相关的。Agent Framework的思路则是给你一套完整的开发框架从 Agent 的定义、编排、记忆、工具、回调到多 Agent 协作全都在这套框架的抽象里完成。你是在框架的世界观里写代码框架帮你把 Runtime 那层也一起管了。这两条路我都实际走过。下面我把整个决策过程、踩过的坑、以及最后为什么做了那个选择完整地摊开讲。如果你正在做 AI Data Agent或者任何需要让模型去操作真实系统的项目这篇应该能帮你少走不少弯路。2. 先搞清楚 AI Data Agent 到底特殊在哪在讨论选型之前得先明确 AI Data Agent 这类应用和普通聊天机器人的本质区别。不搞清楚这个选型就是拍脑袋。2.1 它不是聊天是带副作用的操作普通对话应用模型输出一段文字就结束了最多再加个流式渲染。但 Data Agent 不一样它的每一次行动都可能产生真实副作用执行一条 SQL 可能扫了上亿行数据调用一个导出接口可能生成了一个几百兆的文件触发一个写操作可能改了生产库的数据。这意味着什么意味着执行链路必须是可控、可中断、可审计的。你不能让模型自由发挥到一半发现它要DROP TABLE才去拦。这就要求 Runtime 层对工具调用有非常明确的拦截、校验、超时和回滚机制。我踩过的第一个坑就在这里。早期我用某个 Framework 快速搭了个原型工具调用确实方便一行装饰器就注册好了。但上线前做安全评审时发现我根本没办法在框架的抽象层里干净地插入一个SQL 白名单校验 执行前 dry-run的中间层。框架把工具调用封装得太顺滑了顺滑到我没有地方下手。2.2 上下文里塞的是数据 schema不是闲聊Data Agent 的上下文和普通对话完全不同。它需要把数据库的表结构、字段含义、指标口径、甚至历史查询模式塞进上下文。这些东西体量大、结构强、更新频率还不低。一个中等规模的数仓几百张表、几千个字段是常态。你不可能全塞进 Prompt。所以必须有动态的 schema 检索和裁剪机制——根据用户问题先召回相关的表和字段再组装上下文。这个机制本身就是一个需要精细控制的 Plugin 级能力而不是框架能自动帮你搞定的。2.3 多轮纠错是常态不是例外用户问上个月华东区的销售额Agent 生成的 SQL 可能因为口径理解偏差跑出来一个明显不对的数。这时候用户会说不对我要的是剔除退货的。Agent 需要基于上一轮的 SQL 和结果做增量修正而不是从头再来。这种基于执行结果的多轮状态管理对 Runtime 的上下文管理能力要求很高。框架如果把这层封装得太死你很难干预它到底保留了哪些历史、丢弃了哪些。把这三条摆出来其实选型的判断标准就清晰了谁对执行链路的控制力更强谁更适合 Data Agent。但控制力强往往意味着开发效率低这就是矛盾所在。3. 通用 Runtime Plugin 这条路的真实体验我先讲我实际用 Runtime Plugin 方式做的那个版本因为这条路我走得最久体会也最深。3.1 Runtime 负责什么Plugin 负责什么我当时的划分是这样的Runtime 层负责 Agent 的主循环、模型调用、工具分发、上下文窗口管理、以及最重要的——执行状态机。每一次工具调用都是一个状态节点有 pending、running、success、failed、timeout 几种状态全部落库。Plugin 层则是一个个独立的能力单元。我做了这么几个核心 PluginSchema 检索 Plugin输入用户问题输出相关的表名和字段列表内部用的是向量检索加关键词混合召回。SQL 生成 Plugin拿到 schema 和问题生成候选 SQL内部做了 few-shot 和语法校验。SQL 执行 Plugin负责连接池管理、超时控制、结果行数限制、以及前面说的白名单校验。结果解读 Plugin把查询结果转成自然语言必要时触发图表生成。这么拆的好处非常明显每个 Plugin 可以独立测试、独立替换、独立限流。SQL 执行 Plugin 我可以给它单独配一个连接池和超时策略Schema 检索 Plugin 我可以单独换检索算法互不影响。3.2 为什么这种拆分对 Data Agent 特别友好关键在于边界清晰。Data Agent 最容易出问题的地方恰恰是模型生成的 SQL 直接打到数据库这个环节。在 Runtime Plugin 的架构里这个环节是一个明确的 Plugin 边界我可以在 Plugin 的入口做所有我想做的校验而不需要去理解框架内部是怎么调用的。我举个具体的例子。我在 SQL 执行 Plugin 的入口加了三道闸语法解析用 SQL parser 把语句解析成 AST检查有没有 DDL、DML 操作只允许 SELECT。表名白名单AST 里提取所有表名和配置的白名单比对不在白名单里的直接拒绝。Dry-run 估算对支持的数据库先跑一个 EXPLAIN估算扫描行数超过阈值就要求用户确认。这三道闸加起来不到两百行代码但它们是整个系统安全性的基石。如果我用的是把工具调用封装得很深的 Framework这三道闸要么插不进去要么得用很别扭的方式绕进去。3.3 代价你得自己造不少轮子但这条路不是没有代价。Runtime 层的主循环、上下文管理、重试逻辑、并发控制这些都得自己写或者自己拼。我大概花了三周时间才把 Runtime 层打磨到能稳定跑多轮对话不丢状态的程度。其中最烦的是上下文窗口的动态管理。Data Agent 的上下文里既有对话历史又有 schema 信息又有 SQL 和结果很容易就撑爆窗口。我最后实现了一套基于优先级的裁剪策略schema 信息优先级最高最近两轮的 SQL 和结果次之更早的对话历史按重要性衰减。这套逻辑框架一般不会给你现成的得自己调。还有并发。多个用户同时问数据每个 Agent 实例都在跑自己的循环连接池、模型调用配额、Plugin 的限流这些都得自己协调。我在这块踩过一个坑早期没做 Plugin 级别的并发控制结果一个用户问了个全表扫描的问题把数据库连接池占满了其他用户全部超时。后来给 SQL 执行 Plugin 单独加了信号量限流才解决。4. Agent Framework 那条路我为什么最后没选我不是没试过 Framework。事实上在项目启动的第一周我用当时比较流行的一个 Agent Framework 搭了个能跑的原型两天就出来了。但越往后做越觉得不对劲。4.1 框架的顺滑在 Data Agent 场景下变成了失控Framework 最大的卖点是抽象层次高你定义好 Agent 和 Tool框架帮你处理编排、记忆、回调。在 Demo 阶段这确实爽。但 Data Agent 需要的是在关键节点插入强控制而框架的抽象恰恰把这些节点藏起来了。我举个具体的。我想在模型决定调用 SQL 执行工具和工具真正执行之间插入一个人工确认环节因为有些查询代价太大。在 Framework 里这个插入点非常难找。框架的工具调用是一条内部流水线你要么改框架源码要么用它的回调机制但回调机制给的信息又不够——它只告诉你要调用工具了不给你足够的上下文去做判断。4.2 状态管理是个黑盒Data Agent 的多轮纠错需要精确控制保留哪些历史。Framework 一般有自己的记忆管理策略比如按 token 数截断、按轮数保留。但这些策略是通用的不针对 Data Agent 优化。我遇到的具体问题是用户连续问了三个相关问题框架的记忆策略把第一个问题的 SQL 给截掉了导致第三轮纠错时 Agent 失去了上下文重新生成了一版完全不同的 SQL。这种问题在通用对话里无所谓但在 Data Agent 里是致命的因为用户会觉得你怎么又变了。4.3 调试和可观测性这一点是我最终放弃 Framework 的决定性因素。Data Agent 出问题时我需要知道模型为什么生成了这条 SQLschema 检索召回了哪些表执行耗时多少结果为什么被解读成这样在 Runtime Plugin 架构里每个环节都是我自己的代码日志想怎么打就怎么打链路追踪想怎么埋就怎么埋。但在 Framework 里很多环节是框架内部的我只能看到它暴露出来的那部分。有一次一个查询结果不对我排查了整整一天最后发现是框架在组装上下文时对某个字段做了它自己的处理而这个处理在文档里根本没提。4.4 框架不是不能用而是要看场景我得说句公道话Framework 不是不好。如果你的 Agent 场景是轻量、无副作用、快速验证的比如做个知识问答、做个内容生成助手Framework 能让你几天出活非常划算。但 Data Agent 这类重执行、强控制、需要精细可观测的场景Framework 的抽象反而成了负担。你花在绕过框架限制上的时间可能比自己写 Runtime 还多。5. 一张表看清两条路的取舍说了这么多我把核心差异整理成一张表方便你对照自己的场景做判断。维度通用 Runtime PluginAgent Framework执行链路控制力强每个节点可拦截弱依赖框架暴露的钩子开发速度原型慢需自建主循环快开箱即用开发速度复杂场景前期慢后期稳前期快后期绕状态管理完全自定义框架策略难干预可观测性完全可控受限于框架暴露工具调用安全校验天然支持边界清晰需绕框架实现多 Agent 协作需自己设计框架通常内置维护成本自己代码自己负责依赖框架版本演进适合场景重执行、强控制、Data Agent轻量、快速验证、通用对话这张表不是让你二选一而是让你看清你的场景里哪些维度是刚需哪些可以妥协。Data Agent 里执行链路控制力和可观测性是刚需所以 Runtime Plugin 更合适。但如果你的场景里多 Agent 协作是刚需那 Framework 内置的编排能力可能更省事。6. 如果重来一次我会怎么设计这套架构项目做完我复盘了一下。如果让我重新设计一个 AI Data Agent我会采用一个混合思路用 Runtime Plugin 做骨架但在某些非核心环节借用 Framework 的成熟组件。6.1 骨架必须自己掌控Runtime 层的主循环、状态机、上下文管理这三样必须自己写。不是因为框架写不好而是因为这三样是 Data Agent 的命脉你必须对它们有 100% 的控制权和可见性。我的具体做法是Runtime 只做最纯粹的循环 状态不掺任何业务逻辑。业务逻辑全部下沉到 Plugin。这样 Runtime 可以非常稳定几乎不用改而业务变化都在 Plugin 层消化。6.2 Plugin 的接口设计是关键Plugin 的接口设计我踩过坑。早期我设计的接口太薄只传了输入参数结果 Plugin 内部需要上下文信息比如当前用户、当前会话的历史 SQL时拿不到只能回头改接口。后来我改成每个 Plugin 调用都携带一个ExecutionContext里面包含会话 ID、用户信息、历史工具调用记录、以及一个共享的 scratchpad。这样 Plugin 既能拿到自己需要的上下文又不会耦合到 Runtime 的内部实现。接口大概长这样class Plugin: def execute(self, input: dict, ctx: ExecutionContext) - PluginResult: ...ExecutionContext是只读的Plugin 想写状态就通过PluginResult返回由 Runtime 统一管理。这样保证了状态变更的可追溯。6.3 该借力的时候别硬扛有些能力确实没必要自己造。比如向量检索我用的是成熟的向量库没必要自己实现。比如链路追踪我直接接了 OpenTelemetry也没必要自己写。Framework 里有些组件是可以单独拿出来用的比如它的 Prompt 模板管理、它的模型调用封装。这些和执行控制无关的部分借来用完全没问题。关键是别让框架的编排层接管你的执行链路。6.4 一个具体的目录结构参考我最后落地的项目结构大概是这样agent/ runtime/ loop.py # 主循环 state.py # 状态机 context.py # 上下文管理 plugins/ schema_retrieval/ sql_generate/ sql_execute/ result_interpret/ plugins/base.py # Plugin 基类和 ExecutionContext observability/ tracing.py logging.py这个结构的好处是Runtime 和 Plugin 的边界一目了然。新人接手时看目录就知道哪块是稳定的、哪块是业务相关的。7. 几个只有踩过才知道的细节最后分享几个我在这个项目里踩过的、文档里不会写的坑。第一个坑Plugin 的超时不能只靠 Runtime 统一设。我一开始给所有 Plugin 设了统一的 30 秒超时结果 Schema 检索经常因为向量库冷启动超过 30 秒被误杀而 SQL 执行有时候跑个复杂查询需要 60 秒。后来改成每个 Plugin 自己声明超时Runtime 按声明来管。第二个坑上下文里的 schema 信息要缓存但不能缓存太久。数仓的表结构是会变的缓存太久会导致 Agent 生成引用不存在字段的 SQL。我的做法是缓存加 TTL同时提供一个手动刷新的入口DDL 变更后主动触发。第三个坑多轮纠错时别把上一轮的完整结果塞进上下文。一个查询返回几万行全塞进去直接爆窗口。我的做法是只保留结果的 schema 加前几行样本加上一个统计摘要行数、关键字段的分布。这样既够模型理解又不占地方。第四个坑Plugin 的失败要区分可重试和不可重试。SQL 语法错误重试没意义但连接超时可以重试。我在 PluginResult 里加了一个retryable标志Runtime 据此决定是否重试。这个小小的设计省了很多无谓的重试。第五个坑日志里千万别打完整的 SQL 结果。数据可能包含敏感信息日志里只打 SQL 语句和结果的行数、耗时就够了。这个在安全评审时被重点提过。这套架构跑了大半年中间迭代了十几个 PluginRuntime 层几乎没动过。这大概就是骨架稳、业务活带来的好处。选型这件事没有绝对的对错只有适不适合你的场景。Data Agent 这种重执行的场景把控制权握在自己手里长期看是值得的。