☰
AI工程化四层架构:Prompt/Chain/Graph/Runtime的物理分层实践
2026/10/23 10:11:41 网站建设 项目流程

1. 四层工程不是概念包装,而是任务落地的物理分层

“AI 自动干活”这五个字在2024年已经听腻了——但真正能稳定跑通、周复一周不崩、改需求不重写、换模型不推翻的“自动干活”,至今仍是少数团队手里的硬通货。我带过三支不同行业的AI工程化小组,从金融风控的规则引擎迁移,到电商客服的意图泛化调度,再到工业质检的缺陷归因链路,所有最终跑进生产环境的系统,无一例外都长成同一个骨架:四层结构。它不是PPT里画出来的抽象模型,而是被日志压弯、被超时打穿、被业务方凌晨三点电话逼出来的物理分层。

这四层分别是:Prompt 层 → Chain 层 → Graph 层 → Runtime 层。注意,这里说的“Graph”不是指知识图谱(Knowledge Graph),也不是前端渲染的可视化图(Graph UI),而是指可执行的任务拓扑图(Executable Task Graph)——一种把“用户一句话”翻译成“机器可调度、可中断、可重试、可监控的原子操作流”的中间表示。它和传统工作流引擎(如Airflow、Camunda)的本质区别在于:节点不是预定义的Java类或Python函数,而是由大模型动态生成、经人工校验后固化下来的语义确定性单元(Semantic-Deterministic Unit)。

为什么必须分四层?举个真实案例:上个月我们接了一个“自动整理会议纪要并生成待办事项”的需求。第一版直接用system prompt+few-shot让Qwen-72B输出JSON格式待办,上线三天崩溃两次——一次是模型把“张总确认下周二交付”误判为“张总需在下周二交付”,另一次是遇到跨页表格直接丢段落。后来我们拆开看:问题根本不在prompt写得不够好,而在于整个流程没有隔离“语义理解”和“结构生成”这两个责任。Prompt层只该负责“识别出这是待办事项提取任务”,Chain层才该调用专门的“时间表达式解析器+责任人抽取器+动作动词归一化器”三个子模块,Graph层则把这三个子模块编排成有依赖关系的DAG(比如“责任人抽取”必须等“时间表达式解析”完成才能启动,因为要结合上下文判断“他”指谁),Runtime层负责兜底:当某个子模块失败时,是重试、跳过、还是降级到规则引擎?

这四层之间有清晰的契约边界:

  • Prompt层输出的是任务类型ID + 关键参数锚点(如task_type: "meeting_action_extraction", anchor_time: "next_tuesday", anchor_person: "zhang_zong"),不是原始文本;
  • Chain层接收ID后,加载对应的任务模板(Template),填充锚点,生成Graph DSL描述;
  • Graph层解析DSL,实例化节点(Node)、连接边(Edge)、配置重试策略与超时阈值;
  • Runtime层才是真正干活的:拉起容器、注入上下文、捕获stdout/stderr、上报trace_id、触发告警。

提示:很多团队卡在“Prompt写不好”就反复调参,本质是把本该由Graph层承担的结构稳定性保障,错误地压给了Prompt层。就像要求厨师用菜刀雕出微米级齿轮——不是刀不行,是工种错了。

这四层不是线性流程,而是嵌套反馈环。比如Graph层运行时发现某节点连续三次超时,会触发“降级信号”,反向通知Chain层下次生成Graph时,将该节点替换为轻量规则版本;Runtime层监控到GPU显存抖动,会动态调整Graph中并行节点数。这种闭环能力,才是“自动干活”能长期存活的底层肌肉。

2. Prompt 层:从“写提示词”到“定义任务接口”的范式跃迁

业内还在卷“如何写出惊艳的prompt”,但我们团队已把Prompt层彻底工具化——它现在是一个任务接口定义语言(Task Interface Definition Language, TIDL)编译器。这不是玄学,而是把过去靠经验堆出来的“好prompt”,变成可版本管理、可单元测试、可AB对比的工程资产。

先说清楚一个关键认知:Prompt不是给模型“下指令”,而是给下游Chain层提供结构化输入。所以Prompt层的核心产出物从来不是一段文字,而是三个东西:

  1. 任务类型标识符(Task Type ID):如email_summary_v2,log_anomaly_triage_alpha;
  2. 参数提取结果(Parameter Extraction Map):键值对形式,如{"time_range": "last_7_days", "severity": "critical"};
  3. 置信度分数(Confidence Score):0~1浮点数,用于后续决策(低于0.65则拒绝进入Chain层)。

实现上,我们弃用了纯文本prompt,转而采用Jinja2模板+结构化Schema约束。以“邮件摘要生成”为例,原始prompt可能是:

你是一名资深行政助理,请阅读以下邮件内容,提取核心结论、待办事项、关键时间节点,并用JSON格式输出...

这在工程中是灾难——模型自由发挥空间太大,字段名不统一,缺失值处理不可控。我们的TIDL模板长这样:

{%- set task_id = "email_summary_v2" -%} {%- set schema = { "conclusion": {"type": "string", "required": true, "max_length": 200}, "action_items": {"type": "array", "items": {"type": "object", "properties": { "description": {"type": "string"}, "owner": {"type": "string"}, "due_date": {"type": "string", "format": "date"} }}}, "key_dates": {"type": "array", "items": {"type": "string", "format": "date"}} } -%} 请严格按以下JSON Schema输出,仅输出JSON,不要任何解释: {{ schema | to_json }} 邮件原文: {{ input_text }}

这个模板被编译成两个产物:

  • Prompt字符串:传给大模型的实际文本(含schema描述);
  • Schema验证器:Runtime层收到模型输出后,用jsonschema库校验结构合法性,失败则打日志并触发fallback。

为什么用Jinja2而不是纯JSON Schema?因为需要动态注入上下文。比如当检测到邮件来自财务部时,模板会自动追加一条约束:"budget_impact": {"type": "number", "minimum": 0}。这种条件逻辑,纯Schema做不到。

实操中最大的坑是参数提取的歧义性。例如用户说“查一下昨天的订单”,这里的“昨天”是相对模型推理时间,还是相对用户提问时间?我们强制规定:所有时间参数必须绑定到用户会话时间戳(session_ts),并在Prompt层做标准化转换:

# 在Prompt层预处理阶段执行 def normalize_time_param(raw_text: str, session_ts: datetime) -> dict: # 使用duckling解析自然语言时间 parsed = duckling.parse(raw_text, reference_time=session_ts.isoformat()) if not parsed: return {"time_range": "invalid"} # 统一转为ISO8601区间格式 return {"time_range": f"{parsed[0]['value']['from']}..{parsed[0]['value']['to']}"}

这个函数的输出直接注入Jinja2模板的input_context变量。这样,Chain层拿到的永远是{"time_range": "2024-05-20T00:00:00..2024-05-20T23:59:59"},而不是模糊的“昨天”。

注意:我们禁用所有<|im_end|>、<|eot_id|>等模型专属结束符。所有Prompt模板末尾强制添加<|STOP|>作为唯一终止标记,并在Runtime层设置stop_sequences=["<|STOP|>"]。这是为了防止模型在JSON未闭合时强行续写,导致后续JSON解析器崩溃——这个细节让线上JSON解析失败率从12%降到0.3%。

另一个血泪教训:永远不要让Prompt层承担数据清洗责任。曾有个项目要求Prompt直接过滤掉邮件中的HTML标签,结果模型把<p>紧急</p>错解为“段落紧急”,生成了荒谬的摘要。正确做法是:在Prompt层上游加一个独立的html_stripper预处理器,输出纯文本后再进Prompt。分层的价值,正在于让每个层只做一件事,且这件事做到极致。

3. Chain 层:任务模板的工业化生产线

如果说Prompt层是“识别任务”,那么Chain层就是“组装任务”。它不碰模型,不碰数据,只做一件事:根据Prompt层输出的Task Type ID和参数Map,从模板仓库中加载对应的任务模板(Task Template),填充参数,生成Graph DSL描述。这个过程看似简单,却是整个四层架构中最容易被低估的“承重墙”。

我们不用LangChain这类通用框架,而是自研了一套极简的Chain Engine,核心只有三个概念:

  • Template Registry:一个版本化的模板仓库,每个模板包含template.yaml(元信息)和graph.dsl(图描述);
  • Parameter Binder:将Prompt层输出的参数Map,安全地注入模板的占位符;
  • DSL Compiler:把注入后的graph.dsl编译成Runtime层可执行的JSON Schema。

以“服务器日志异常根因分析”任务为例,其template.yaml长这样:

name: log_anomaly_triage_alpha version: 1.3.2 author: infra-team compatible_models: ["qwen-72b", "llama3-70b"] input_schema: time_range: string # ISO8601区间 severity: enum[critical, high, medium] output_schema: root_cause: string affected_services: array[string] remediation_steps: array[string]

对应的graph.dsl定义了执行流程:

node "parse_log_lines" { type = "log_line_parser" config = { time_range = "${input.time_range}" } } node "extract_errors" { type = "error_extractor" config = { min_severity = "${input.severity}" } depends_on = ["parse_log_lines"] } node "search_knowledge_base" { type = "kb_searcher" config = { query = "${extract_errors.error_codes}" } depends_on = ["extract_errors"] } node "generate_report" { type = "report_generator" config = { root_cause = "${search_knowledge_base.root_cause}" } depends_on = ["search_knowledge_base"] }

看到这里你可能想问:为什么不直接用YAML写DAG?因为YAML无法表达动态分支逻辑。比如当severity == "critical"时,需要额外插入一个alert_pagerduty节点,而YAML做不到条件编译。我们的DSL支持Jinja2语法:

{%- if input.severity == "critical" -%} node "alert_pagerduty" { type = "pagerduty_alert" config = { incident_title = "CRITICAL: ${extract_errors.error_codes}" } depends_on = ["search_knowledge_base"] } {%- endif -%}

这个DSL在Chain层被编译时,会先执行Jinja2渲染,再解析成标准JSON。整个过程在毫秒级完成,且可单元测试——我们为每个模板编写了test_template.py,用Mock参数验证编译后的JSON结构是否符合预期。

最常踩的坑是参数注入的安全边界。早期版本允许用户输入任意JSON Path作为参数名,结果有人传入../../../etc/passwd导致模板读取了系统文件。现在我们强制所有参数名必须通过白名单校验:

# Chain Engine核心校验逻辑 WHITELISTED_PARAM_KEYS = { "time_range", "severity", "service_name", "error_code", "user_id", "region", "environment" } def validate_param_keys(params: dict) -> bool: for key in params.keys(): if key not in WHITELISTED_PARAM_KEYS: logger.warning(f"Invalid param key: {key}") return False return True

另一个关键设计是模板版本灰度发布。新模板上线不直接全量,而是按session_id % 100分流:0-9号用户走v1.3.1,10-19号走v1.3.2。我们监控两组的graph_execution_time和fallback_rate(降级率),达标后才全量。这个机制让我们在两周内发现了v1.3.2在处理environment=staging时,kb_searcher节点会因超时被Runtime层强制kill——问题定位到DSL中一个未设timeout的config字段,修复后重新发布。

提示:Chain层的性能瓶颈从来不在计算,而在IO。我们把所有模板缓存在Redis中,Key为template:{name}:{version},TTL设为7天。首次加载时从GitLab CI构建产物桶下载,后续请求直接从Redis读取。实测模板加载耗时从平均320ms降到8ms。

最后强调一个原则:Chain层绝不调用大模型。所有LLM调用必须下沉到Graph层的特定节点(如llm_summarizer)。Chain层只做“静态组装”,确保其执行是确定性的、可预测的、可回滚的。这是工程稳定性的底线。

4. Graph 层:让AI任务具备“可编程性”的核心枢纽

Graph层是四层架构中技术密度最高的环节,也是最容易被误解为“炫技”的部分。很多人以为Graph就是画个流程图,但真正的Graph层要解决三个硬核问题:节点可插拔、边可编程、状态可追溯。它不是把Prompt翻译成图,而是把“人类任务逻辑”翻译成“机器可执行的拓扑协议”。

我们采用自研的GraphIR(Intermediate Representation)作为核心协议,它是一个精简的JSON Schema,定义了节点、边、配置的最小完备集。一个典型的GraphIR实例长这样:

{ "graph_id": "g-20240521-001", "nodes": [ { "node_id": "n1", "type": "log_line_parser", "config": { "time_range": "2024-05-20T00:00:00..2024-05-20T23:59:59", "batch_size": 1000 }, "timeout_ms": 15000, "retry_policy": { "max_attempts": 2, "backoff_ms": 1000 } }, { "node_id": "n2", "type": "error_extractor", "config": { "min_severity": "critical" }, "timeout_ms": 8000, "depends_on": ["n1"] } ], "edges": [ { "from": "n1", "to": "n2", "condition": "output.line_count > 500" } ] }

注意edges中的condition字段——这才是Graph层的灵魂。它允许节点间建立数据驱动的动态分支,而非静态DAG。比如当log_line_parser输出的日志行数超过500时,才触发error_extractor;否则跳过,直接走generate_report。这种条件逻辑,是纯Chain层无法表达的。

Graph层的实现分三步:

  1. IR解析器:把Chain层生成的DSL编译成GraphIR;
  2. 拓扑验证器:检查是否有环、是否有悬空节点、条件表达式是否合法;
  3. 执行计划生成器:把GraphIR转为Runtime层可调度的Plan(含节点优先级、资源需求、容错策略)。

最关键的验证环节,我们引入了形式化验证思想。对每个GraphIR,生成一个Petri网模型,用Z3求解器验证:

  • 是否存在死锁状态(所有节点都在等彼此)?
  • 是否存在不可达节点(因条件永远为假而永不执行)?
  • 超时配置是否满足传递性(A依赖B,则A.timeout > B.timeout + network_overhead)?

这个验证在CI阶段强制执行,失败则阻断发布。去年Q3我们拦截了17个存在隐式死锁的Graph模板,其中最危险的一个是:node_A等待node_B的输出,而node_B的条件依赖node_A的某个统计字段——典型的循环依赖。

Graph层的节点类型分三类:

  • 原子节点(Atomic Node):如http_caller、sql_executor,封装单一能力,无内部状态;
  • 代理节点(Proxy Node):如llm_summarizer,负责调用大模型,但把Prompt、Schema、重试逻辑全部封装在内部;
  • 聚合节点(Aggregator Node):如parallel_fanout,将输入分发到N个子Graph并行执行,再合并结果。

聚合节点是处理复杂任务的关键。比如“多源日志联合分析”任务,需要同时解析K8s Pod日志、Nginx访问日志、数据库慢查询日志。我们不写一个巨无霸Graph,而是定义一个multi_source_analyzer聚合节点,其内部包含三个子Graph,每个子Graph专注一种日志格式。主Graph只关心聚合节点的输入/输出契约,完全屏蔽子Graph的实现细节。

实战心得:Graph层最大的性能杀手是节点间的数据序列化开销。早期我们用JSON.stringify()传递中间结果,当log_line_parser输出10MB日志行时,序列化+反序列化耗时占到总执行时间的43%。解决方案是:对大于1MB的数据,自动生成临时S3 URL,节点间只传递URL字符串,由Runtime层自动下载。这个优化让大日志任务平均耗时下降68%。

另一个血泪教训:永远为Graph设置全局超时(global_timeout)。我们曾有个任务Graph未设此参数,当kb_searcher节点因网络抖动卡住时,整个任务无限挂起,占满Worker队列。现在所有GraphIR强制要求global_timeout_ms字段,Runtime层在启动时注入一个守护协程,超时即强制终止所有子进程并清理资源。

5. Runtime 层:让AI任务像Linux进程一样可靠运行

Runtime层是四层架构的“操作系统内核”,它不关心任务逻辑,只确保Graph被确定性、可观测、可恢复地执行。如果说前三层是“写代码”,Runtime层就是“跑代码”的Linux内核——它提供进程管理、内存调度、信号处理、日志审计等底层能力。很多团队失败,是因为把Runtime层当成简单的“调用LLM API”,却忽略了AI任务特有的脆弱性:模型响应不稳定、token长度突变、GPU显存碎片化、网络抖动等。

我们的Runtime基于Rust编写,核心组件包括:

  • Executor Manager:管理Worker进程池,支持CPU/GPU混合调度;
  • State Tracker:为每个Graph执行实例维护完整状态机(Pending → Running → Succeeded / Failed / Cancelled / Timeout);
  • Trace Collector:集成OpenTelemetry,记录每个节点的start/end时间、输入/输出摘要、错误堆栈;
  • Resource Guardian:实时监控GPU显存、CPU负载、磁盘IO,动态调整并发数。

一个Graph执行实例在Runtime层的生命周期如下:

  1. 接收GraphIR,生成唯一execution_id(如exec-20240521-abc123);
  2. 根据GraphIR中各节点的resource_requirement(如{"gpu_memory_mb": 4000, "cpu_cores": 2})分配Worker;
  3. 启动沙箱进程,注入execution_id、trace_id、parent_span_id;
  4. 按拓扑顺序调度节点,每个节点启动前检查资源水位;
  5. 节点执行时,Runtime截获stdout/stderr,提取结构化日志(如[NODE:n1] STARTED);
  6. 执行结束,上传完整trace到Jaeger,保存状态到PostgreSQL。

最关键的创新是状态快照(State Snapshot)机制。每个节点执行完成后,Runtime自动序列化其输出到S3(路径为s3://runtime-snapshots/{execution_id}/{node_id}/output.json),并记录SHA256哈希。这样当任务失败时,无需重跑整个Graph,只需从失败节点的上一个快照恢复。我们实测:一个12节点的Graph,第10节点失败后,从快照恢复平均耗时2.3秒,而重跑全程需87秒。

另一个硬核功能是动态资源熔断。Runtime持续采集Worker的GPU显存使用率,当某Worker的gpu_memory_utilization > 92%持续5秒,立即触发熔断:

  • 拒绝新任务分配到该Worker;
  • 对已在执行的任务,若其节点配置了allow_gpu_fallback: true,则将其降级到CPU执行;
  • 向Prometheus推送runtime_worker_gpu_overload{worker="w-05"}指标,触发告警。

这个机制让我们在GPU集群负载峰值期(如每日凌晨2点模型批量推理),仍能保障99.95%的AI任务SLA。没有它,高峰期任务失败率会飙升至18%。

运维层面,Runtime层提供了三个救命命令:

  • runtimectl status --execution-id exec-20240521-abc123:查看当前执行状态、各节点耗时、资源占用;
  • runtimectl cancel --execution-id exec-20240521-abc123:安全终止执行(会等待当前节点完成再退出);
  • runtimectl replay --execution-id exec-20240521-abc123 --node-id n7:从指定节点快照重放,用于调试。

注意:所有节点输出必须经过输出规范化(Output Normalization)。比如llm_summarizer节点可能返回{"summary": "xxx"}或{"result": "xxx"},Runtime层在保存快照前,强制映射为统一Schema:{"normalized_output": {"summary": "xxx"}}。这个步骤在state_tracker模块中实现,避免下游消费时写一堆if-else判断字段名。

最后分享一个真实故障处理案例:某天下午3点,大量log_anomaly_triage任务超时。通过runtimectl status发现,所有失败任务都卡在kb_searcher节点。进一步查trace_collector日志,发现该节点发起的HTTP请求返回503 Service Unavailable。原来知识库服务因流量突增自动扩容,但新Pod的 readiness probe 配置错误,导致部分Pod被加入Service却无法响应。我们立刻:

  1. 在Runtime层配置kb_searcher节点的fallback_strategy: "use_cached_results";
  2. 通知Infra团队修复probe;
  3. 用runtimectl replay重放失败任务,全部成功。
    整个过程12分钟,业务无感知。这就是Runtime层作为“操作系统”的价值——它不解决业务问题,但为解决业务问题提供确定性基础。

6. 四层协同实战:从“一句话需求”到“生产就绪系统”的完整链路

现在,让我们把四层串起来,用一个真实项目演示如何从零落地。项目背景:某跨境电商平台需要“自动分析用户退货原因并生成改进报告”,替代原先人工每周汇总Excel的流程。需求方只给了三句话:

  • “看最近7天退货订单,找出高频原因”;
  • “按商品类目分组统计”;
  • “生成PPT格式报告,重点标红TOP3问题”。

整个开发周期11天,上线后准确率92.4%,处理时效从4小时缩短到83秒。以下是四层如何分工协作:

6.1 Prompt 层:把模糊需求转为结构化输入

我们设计了return_reason_analysis_v1模板,核心约束:

  • 强制要求输出JSON,字段为{"time_range": "ISO8601", "group_by": "category|brand|sku"};
  • 内置时间解析器,将“最近7天”转为2024-05-14T00:00:00..2024-05-20T23:59:59;
  • 添加防幻觉提示:“若未找到退货原因数据,输出{"reasons": []},不要编造”。

上线首日,Prompt层拦截了17%的无效输入(如用户说“查一下昨天的退货”,但数据库无昨日数据),避免错误流入下游。

6.2 Chain 层:组装分析流水线

根据Prompt层输出的group_by: "category",Chain层加载return_analysis_category_template,生成Graph DSL:

  • fetch_orders节点:从ClickHouse查退货订单;
  • extract_reasons节点:调用微服务解析退货原因文本;
  • aggregate_by_category节点:按类目聚合频次;
  • generate_ppt节点:用python-pptx生成报告。

特别设计:当fetch_orders返回订单数<100时,DSL自动插入skip_ppt_generation节点,直接输出“数据量不足,暂不生成报告”。

6.3 Graph 层:注入业务规则与容错

GraphIR中为extract_reasons节点配置:

  • timeout_ms: 30000(原因解析较慢);
  • retry_policy: {"max_attempts": 1, "backoff_ms": 5000};
  • fallback_strategy: "use_rule_based_parser"(当LLM节点失败,降级到正则匹配)。

为generate_ppt节点配置resource_requirement: {"cpu_cores": 4, "memory_mb": 8192},确保PPT生成不OOM。

6.4 Runtime 层:保障生产稳定性

  • 设置全局超时global_timeout_ms: 120000;
  • fetch_orders节点启用streaming: true,避免10万订单一次性加载到内存;
  • 所有节点输出快照存S3,路径含execution_id和node_id;
  • 集成Datadog,监控runtime_graph_execution_time{task="return_analysis"},设置P95>90s告警。

上线后第三天,监控发现extract_reasons节点P95耗时突然升至42秒。查Trace发现,某类退货原因文本含大量emoji,导致LLM token数暴增。我们立刻:

  1. 在Prompt层模板中增加约束:“移除输入文本中的emoji和特殊符号”;
  2. 在GraphIR中为该节点增加preprocess: "remove_emoji"配置;
  3. 用runtimectl replay重放历史失败任务,全部通过。

整个修复过程27分钟,无需重启服务。

这个案例印证了四层架构的核心价值:每一层的变更都可独立进行,且影响范围可控。改Prompt层只需更新模板;调优Graph层只需修改IR配置;升级Runtime层只需滚动更新Worker镜像。没有哪一层的改动会牵一发而动全身。

最后分享一个团队共识:四层不是银弹,它解决的是“如何让AI任务规模化、可持续地交付”,而不是“如何让单次AI调用更准”。如果你的场景是单点POC、或者需求月月变、或者没有工程化运维能力,强行套用四层只会增加负担。我们只在明确要上生产、要支撑百人以上业务方、要保证99.9%可用率时,才启动四层架构。毕竟,工程化的终极目标不是炫技,而是让“自动干活”这件事,变得像拧开水龙头一样确定、简单、可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询