给虚拟工厂装一个 Agent:对话与批量双编排、自描述工具、可控写入的架构设计

上一篇讲了我们那条用 .NET 跑的虚拟工厂。这一篇讲架在它之上的 AI Agent:一个能听懂"把这个配方配成一条产线、再给每个工位生成工艺脚本"并真把活干完的系统。

接一个 LLM 是这套东西里最不值一提的部分。真正的工程量在于:怎么把它架成一个能干活、能并行、能流式反馈、还不会把生产配置改坏的系统。这篇就讲这套架构------整体怎么分、为什么是两套编排、批量任务内部怎么流水化、脚本怎么生成又怎么保证可用、写入怎么做到可控,以及每个决策换来的好处。

一、架构目标

先把这个 Agent 要满足的设计目标定清楚,后面所有结构都是为它们服务的:

  • 职责分层:模型只做它擅长的------理解意图、规划步骤、生成文本;确定性的活全部下沉到代码,保证可复现。
  • 能力可插拔:给 Agent 加一个本领,应该是加一个工具,而不是改 Agent 本身。
  • 写入可控:任何对生产配置的修改都必须可审、可挡,模型不能直接落盘。
  • 生成可用:模型生成的脚本不能只是"看起来像",必须保证只引用真实存在的变量。
  • 可并行 + 可反馈 + 可中止:几十个工位的批量任务要能并行跑,进度要实时回流,用户还要能随时干净地停下来。

二、系统结构与一次请求的生命周期

系统是两个独立进程:一个 Agent (Node + TypeScript,NestJS,用 LangChain / LangGraph 编排,模型走 OpenAI 兼容协议),一个 MCP 工具服务(Go,通过 HTTP 暴露一组操作工厂配置的工具)。

但比"有哪些组件"更重要的是一次请求怎么流过它们:

graph TD UI["对话页"] -->|对话请求| Ctl["Chat 控制器"] Ctl --> Mem["记忆装配:滚动摘要 + 最近三轮"] Mem --> Agent["LangChain Agent 编排"] Agent -->|流式 token| LLM["对话模型"] Agent -->|本地工具| Local["脚本生成、模板渲染、交互"] Agent -->|远程工具| Mcp["MCP 客户端"] Mcp -->|JSON-RPC over HTTP| Go["MCP 服务(Go)"] Go --> FS["产线配置、脚本、Excel 资源"] Agent -->|批量任务| Wf["LangGraph 工作流"] Wf -.进度事件.-> Ctl Agent -.token 与活动.-> Ctl Ctl -->|SSE| UI

一条消息进来,先经过记忆装配,再交给 Agent 编排;编排过程中模型自主决定调哪些工具------本地工具直接执行,涉及工厂数据的走 MCP 客户端、转成 HTTP 上的 JSON-RPC 打到 Go 服务;整个过程的 token、工具活动、批量进度,全部以 SSE 事件实时推回前端。下面逐段拆这条生命周期里的关键决策。

三、核心决策:为什么是对话、批量两套编排

这是整个 Agent 最重要的架构选择。系统里并存两套编排,服务于两种本质不同的控制流。

对话用 LangChain 的 createAgent 用户随便问一句,下一步该查什么、调哪个工具,是模型临场决定的------这是模型驱动的控制流,适合一个会自主调工具的 agent loop。所有工具(本地的 + 动态加载的 MCP 工具)平铺给它。

批量给几十个工位写脚本,用 LangGraph 的 StateGraph 这件事的控制流是确定的 :planner 把所有工位排成队列,batch manager 按滑动窗口取一批,每批扇出成多个并行 worker(一工位一个),干完回到 manager 取下一批,全部完成进 assembler 收尾。

graph LR S[开始] --> P[planner 建任务队列] P --> B[batch manager 取一批] B -->|批非空| W["worker · 每工位并行"] W --> B B -->|队列空| A[assembler 收尾] A --> E[结束]

为什么不把批量也丢给对话 agent 一个个调工具?因为这正是 agent loop 的短板:几十个工位的长链路里,模型容易漏掉一个、或顺序乱掉,而且无法并行、无法精确控流。把确定的控制流交给图来表达,换来三个实打实的好处 :滑动窗口控制并发(默认 min(5, CPU-2))、扇出并行、给执行设步数上限。

这个步数上限本身也是个值得讲的设计。LangGraph 为防死循环会限制递归步数,我们没把它写成常量------常量在大产线下会被框架提前掐断,脚本生成到一半停掉。正确做法是让它随工位规模缩放:

ts 复制代码
const batchCount = Math.ceil(workstationCount / Math.max(1, batchSize));
return normalizeRecursionLimit(Math.max(100, batchCount * 6 + 20)); // 下限 25,留环境变量兜底

好处是任意规模的产线都能跑完,不需要每次手调一个魔法数字。

四、批量工作流的内部:一个工位是怎么被处理的

上面那张图只是骨架。真正体现工程密度的,是一个 worker 处理单个工位时走的那条流水线。这也是整个产品的心脏,值得细看。

worker 先对工位分类:标准工位 (动作能匹配到模板流程)和非标工位(需要人补逻辑)。标准工位的每个动作,走的是这样一条严格有序的链:

graph TD C[&#34;工位分类<br/>标准 / 非标&#34;] -->|标准动作| K[&#34;ensure_action_skeleton<br/>幂等建动作骨架&#34;] K --> EN{&#34;该工艺模型<br/>补过点位吗&#34;} EN -->|没有| E1[&#34;prepare/confirm<br/>闭合点位引用环&#34;] EN -->|已补| R E1 --> R[&#34;渲染脚本<br/>模板 + 取可用变量 + 模型绑定&#34;] R --> V[&#34;静态校验<br/>变量是否都在允许集内&#34;] V --> WR[&#34;write_process_script(confirmed)&#34;] C -->|非标动作| H[&#34;进 pendingCustomStations<br/>交人在环&#34;]

每一步背后都有明确的设计意图:

先建骨架,再填脚本。 ensure_workstation_action_skeleton 是幂等的------它只保证"这个工位的这个动作的绑定骨架存在",并返回对应的工艺模型码,但不碰脚本内容。把"骨架"和"脚本"拆成两步,好处是整个批量过程可重入:跑到一半中断,重跑时骨架已存在不会重复创建,只补没写完的脚本。这对几十个工位的长任务是必需的容错。

点位补全按工艺模型去重。 多个工位可能共用同一个工艺模型,而"补全点位引用环"(把工艺模型的点位定义和工位的变量映射对齐闭合)是工艺模型级的操作。worker 用一个集合记住"这个工艺模型补过了",同模型只补一次。好处是省掉大量重复的 MCP 调用,批量越大省得越多。这一步本身也走的是两阶段 prepare/confirm(下一节细讲),不会直接改配置。

渲染脚本,但模型只做填空。 这一步取来标准动作模板、取来"该工位实际可用的变量",用 Handlebars 把模板结构填好,再让模型把模板里的占位符绑定到真实的点位 key 。注意模型在这里的权限是被严格限制的------它只能用 allowedVariables 里真实存在的 key。

生成完立刻静态校验。 这是我觉得最关键的一道防线。脚本生成出来不等于能用,模型完全可能"幻觉"出一个不存在的变量名。所以 validateGeneratedScript 会把生成脚本里出现的每一个 ReadCmsAsync/WriteCmsAsync 的变量、每一个 Parameters[...] 的参数 key、每一个 journal key 都抠出来,逐个比对是否在允许集合内,返回 invalidCmsVariables / invalidParameterKeys / invalidJournalKeys。好处直接:写进产线的脚本,一定只引用真实存在的点位和参数,模型的幻觉在落盘前就被挡住了。

每一步之间都有取消检查点。 代码里每个 await 之间穿插着 assertNotAborted(abortSignal)。好处是用户随时能中止一个跑到一半的批量任务,而不会留下半成品------这是"可中止"目标在最细粒度上的落地。

非标动作则不硬塞:它们被收进 pendingCustomStations,交给人在环处理(第八节讲)。该自动的全自动,该问人的精确问人,这条线就划在 worker 的分类这一步。

五、工艺脚本怎么生成:模板 + 双模型 + 约束

脚本生成这件事,我没有用"把需求丢给模型让它自由发挥"的做法,而是搭了一套约束生成的架构。

底座是一组标准动作模板------进站、出站、加工、物料检测、物料扣减、生成码、配方下发、标签打印、托盘绑定/解绑,十个常见动作各一个 CSX 模板。Handlebars 负责把模板的结构 渲染出来,模型只负责把占位符绑到真实变量。结构由模板保证,变量由模型填,两者职责分明。

模型这一侧也做了拆分:对话用一个模型(温度 0.7,需要一点灵活),脚本生成用一个独立的模型,温度压到 0.2。原因很简单------生成代码要的是确定性和稳定,不是创造性。两套模型各调各的参数,可以分别用环境变量配置,甚至指向不同的模型。

除了模板路径,还有一条面向自由生成/精修的脚本 agent 路径(generator / refiner 两个角色),靠独立的提示词驱动,用来处理模板覆盖不到的非标逻辑。但无论哪条路径,产物都要过上一节那道 validateGeneratedScript 校验。

把这套架构一句话概括:用模板锁结构、用低温模型锁稳定、用静态校验兜底------让生成式能力在一个封闭、可验证的空间里工作。 这是把 LLM 真正用进生产配置的前提。

六、数据底座:Excel 怎么变成可查询的资源

制造业里,工艺数据的源头往往是 Excel------配方表、工位表、工序表。Agent 不直接面对这些原始表格,中间隔着一层资源化管道

Go 服务用 excelize 把上传的 Excel 解析开,按配方/工位/工序拆成一份份 JSON 资源,各自带 index.json 索引,落到独立的资源目录。之后所有查询类工具读的都是这些结构化 JSON,而不是原始表格;工序流程资源还会和工位资源交叉引用(由工位反查它所属工序绑定的流程)。

这层抽象的价值有两面:一是确定性 ,Excel 只解析一次、一次性资源化,后续查询面对的是稳定结构,不受表格格式波动影响;二是为确定性同步提供依据,像"基于配方生成工序路线"这种工具,正是靠交叉引用这些资源,才能把工序名可靠地映射到真实工位码。模型面对的是一个干净的、可查询的资源层,而不是一堆需要现场理解的单元格。

七、MCP:把工具做成自描述的独立服务

工厂数据的所有读写,我没写成 Agent 里的 TS 函数,而是单拆成那个 Go 服务,通过 MCP 协议接入。这个边界对应三个好处:

职责隔离 + 可复现。 Agent 端只干模型擅长的事;逻辑确定的活下沉到 Go,输入定输出定,不让模型每次重新推理。

能力可插拔。 MCP 的 tools/list 让每个工具自描述------自带名字、说明、参数 Schema。Agent 启动时拉一份清单回来就知道有哪些能力,不在代码里硬编码任何工具签名。于是 Agent 的能力边界等于工具集的边界,加能力 = 加工具,不动 Agent。客户端还会把工具的 JSON Schema 动态转成 Zod,让远程工具获得和本地工具一样的入参校验。

语言无关 + 无状态。 Excel 解析、文件操作、路径安全校验用 Go 更顺手;服务自身不连库、不持状态,只读写文件资源,可随时重启、水平扩。

这个 Go 服务没用任何 MCP SDK,JSON-RPC 是手写的,整个服务就三条路由(健康检查、POST /mcp 分发 tools/listtools/call、Excel 解析)。对一个工具集稳定、只走 HTTP 的内部服务,手写比引依赖更可控。从架构上看,MCP 不是 Agent 的外挂插件,而是它可独立演进、独立部署的能力层。

八、写入安全 + 人在环:模型可控的两道关

模型会犯错是前提,所以系统在"改配置"和"做决策"两处都给了人介入的结构。

改配置:两阶段提交。 凡是会修改产线配置的工具,都拆成 prepare / confirm 两步。prepare 只读数据、算出"打算改成什么样",写进缓存返回给模型和人看,不落盘 ;confirm 才真正写进正式配置,且必须显式带 confirmed=true,再加文件名安全正则挡路径注入。

sequenceDiagram participant A as Agent 模型 participant M as MCP 服务 participant C as 缓存 participant S as 正式配置 A->>M: prepare 提交参数 M->>M: 读数据 + 算变更计划 M->>C: 写计划并返回 planId M-->>A: 计划返回供审阅 A->>M: confirm 携带 planId 与 confirmed=true M->>S: 校验后才落盘

这把模型的每一次写,从"它说改就改"变成了"先出提案、确认了才执行"。AI 出乱子的场景本质都是"没人审就改了不该改的";两阶段从结构上消除了这种可能。

做决策:两种人在环。 一种是对话里的结构化交互------模型调 request_agent_interaction,带上交互类型(单选、选目标产线、确认落地、补非标工位上下文),返回一个标记,经 SSE 的 interaction_required 事件让前端渲染对应控件,用户点完再把结果回传,对话继续。另一种发生在批量图内部:遇到非标工位,human-input 节点用 LangGraph 的 interrupt 把整个批量流程真正挂起 ,返回待补工位清单,等人给出 {工位: 补充说明} 再从断点恢复。

后者尤其关键:批量任务不是遇到搞不定的就放弃重来,而是能在图的中途暂停、等人补完、再继续。标准工位自动跑、非标工位停下来精确问人,两类工位在同一条流水线里各得其所。

九、几个支撑性的架构机制

主干能转起来,还靠几个底层设计:

上下文穿透(AsyncLocalStorage)。 Agent 是进程级单例,MCP 工具也只加载一次,但每个请求的会话 ID、目标产线、取消信号都不同,不可能层层透传到深埋在工具调用栈里的请求。于是用 Node 的 AsyncLocalStorage 把它们挂在异步上下文里。好处是:同一批工具实例在不同异步上下文里自动拿到正确的会话信息,而代码里看不到一个 sessionId 在到处传------不同会话的资源落在各自隔离的目录,模型不需要知道路径,客户端按上下文自动补全。

全链路可取消。 取消信号也走异步上下文,配合流水线里密布的 assertNotAborted 检查点,让任意长任务都能随时干净中止。

滚动摘要记忆。 不把全部历史塞给模型:最近三轮原样保留,更早的压成一份滚动摘要注入,并标注"与最近对话冲突时以最近为准"。好处是长对话既不爆 token,也不会让旧摘要盖掉新事实。

十、流式架构:把"模型吐字"和"后台进度"并到一条 SSE

最后一段反馈通道。Agent 的输出是流式的,背后有个不显眼但关键的设计:同一条 SSE 通道,要同时承载两种来源完全不同的事件

一种是对话 agent 的实时输出,用 streamEvents({ version: "v2" }) 拿到原始流,归一成 delta(文本增量)、activity(工具开始结束)、token_usageinteraction_requireddone。另一种是 LangGraph 批量工作流的进度:批处理跑在后台,每完成一个工位、进一个阶段都吐进度事件,这条流被单独消费,再和对话流合并统一往 SSE 写。

graph LR A[&#34;对话流 · streamEvents v2&#34;] --> M[合并] B[&#34;批处理进度流 · WorkProgress&#34;] --> M M -->|归一后的 SSE 事件| SSE[单条 SSE 通道] SSE --> UI[前端]

为什么费劲合流?因为触发批量任务后只能干等最终结果,体验是崩的。合流之后,用户能实时看到"第 12 个工位脚本已生成""这一批 5 个并行处理中",几十个工位的长任务变得透明可感知,配 15 秒心跳防断连。这是"可反馈"目标的落地。

十一、小结:边界感就是这个 Agent 的架构

回看整套设计,让它能真正用在生产配置上的,不是模型多强,而是几条清晰的架构边界:

  • 双编排:模型驱动对话,确定性图驱动批处理,各用其长。
  • 流水线化的批处理:建骨架、补点位、渲染、校验、写入,每步幂等可重入、可取消。
  • 约束生成:模板锁结构、低温模型锁稳定、静态校验挡幻觉,生成的脚本一定可用。
  • 数据资源化:Excel 一次性确定性资源化,模型面对干净的查询层。
  • MCP 能力层:工具自描述、独立服务、可插拔,加能力不动 Agent。
  • 两道人工关:写配置两阶段提交,做决策两种人在环。
  • 流式合并:对话与批处理进度并到一条 SSE,长任务全程透明。

说到底,给 AI 做产线配置这种事,它的边界感比它的能力更重要,而这些边界,恰恰就是这个 Agent 的架构本身。

相关推荐
老梁agent3 小时前
MCP 协议实战:用标准化方式让 Agent 调用工业工具
物联网·agent·mcp
user4465117917914 小时前
从任务树到自我修正:XAgent Plan 数据结构与 Agent 协作机制
agent
武子康4 小时前
调查研究-202 SGLang 深度解析:为什么大模型推理框架不只是“把模型跑起来“
人工智能·openai·agent
前端双越老师4 小时前
我从 0 开发的 AI Agent 智语项目发布了
前端·node.js·agent
沉默王二5 小时前
DeepSeek这次招得太猛了,36个岗位,80%都要会Agent!
agent·ai编程·deepseek
古茗前端团队5 小时前
AI 乱改代码?试试这套 SDD 规范驱动工作流
agent
倾颜9 小时前
给受控 Agent 加 HITL:从 Graph interrupt 到 PostgreSQL checkpoint resume
agent
九酒15 小时前
AI Agent 开发踩坑记:口播功能非得用 APP 原生实现吗?
前端·人工智能·agent
Jackson__16 小时前
做了一段时间的AI coding后,我终于搞清了 CLI 和 MCP 的区别
前端·agent·ai编程