本文以一个真实项目「sysgen-platform」为基础,复盘其智能体编排、RAG 检索链路、MCP 接入与契约体系、产物不可变与级联失效等核心设计与踩过的坑。

一、项目背景与技术栈

1.1 项目简介

sysgen-platform 是一个「AI 智能体生成平台」:用户只需输入一句业务需求(例如"我要一个图书管理系统"),平台就会自动完成领域研究、架构设计、实体建模、动作设计、体验设计、界面设计,最终生成一套可运行的系统蓝图(Blueprint)

它把「LLM 智能体编排」这一个工业级场景拆得足够深,复刻了生产级 LLM 应用开发中几乎所有的典型问题:

  • 多阶段智能体编排(LangGraph 状态图 + 显式路由)
  • 状态持久化与中断恢复(Checkpoint + DB 租约 + 单实例 Leader)
  • 意图修复(定向补丁修复,绝不重跑全阶段)
  • RAG 检索链路(多工具召回 + 证据融合)
  • MCP 工具接入(默认禁用、身份隐藏、写工具拦截)
  • 产物不可变与级联失效(content_hash + input_fingerprint)
  • 提示词 / 契约 / 语义审查的工程化(契约注册表 + 分级 Critic)
1.2 技术栈一览
层级技术选型选型理由
后端框架FastAPI(异步)原生 async/await,配合 SQLAlchemy 2.0 async 生态;Pydantic v2 做契约校验
编排引擎LangGraph / LangChain状态图原生支持 Checkpoint、Command 显式路由、子图嵌套,是 LLM 流程编排的事实标准
数据契约Pydantic v2每个 Artifact 都是强类型模型,model_config = ConfigDict(extra="forbid") 严格拒绝多余字段
主数据库PostgreSQL存放生成 run / artifact 修订 / 蓝图;pg_try_advisory_lock 做单实例 Leader 选举
向量检索Milvus / ZillizRAG 检索:Dense 向量 + BM25 稀疏 → RRF 融合 → Rerank 重排
LLMDashScope(qwen-max / qwen-plus)全部节点统一 qwen-max;兼容 OpenAI 协议端点
网页检索Firecrawlweb_search 工具,返回不透明 result_id 防止模型伪造/越界访问
工具接入MCP多 Server 工具接入,默认禁用,写工具黑名单多层拦截
平台端Vue 3 + TS生成任务、系统列表、蓝图可视化
租户运行时端UniApp / Vue 3 + TS生成的系统在租户端渲染运行
1.3 模块拆分

后端按"编排层 / 生成层 / 运行时"纵向切开,各自职责单一:

模块职责
app/generation_graph/编排层:根图 + 子图拓扑、节点 Handler、FailureRouter、Checkpoint、Leader 选举
app/generation/生成层:Agent 契约、Prompt 体系、契约注册表、Artifact 契约模型、GraphCompiler
app/generation_runtime/运行时层:ArtifactStore(不可变修订)、依赖追踪、租约存储
app/platform/平台 API:系统列表、仪表盘、生成任务管理
app/mcp/MCP 配置与注册表:连接器加载、能力暴露

二、整体架构总览

2.1 架构分层

基础设施

生成引擎 · generation_graph

客户端层

运行时 · generation_runtime

ArtifactStore 不可变修订

租约 + Leader

生成层 · generation

Agent 工厂/契约

提示词体系

契约注册表 InvariantRegistry

平台端 Vue3

API

租户运行时端

根图 RootGenerationState

requirement_research

architecture

entity

action

experience

presentation

integration

PostgreSQL

Redis

Milvus/Zilliz

DashScope qwen

MCP Server

Firecrawl

根图只做一件事:把 7 个子图按顺序串起来,并在每个子图的失败 / 成功之间路由。真正的业务逻辑(Handler、Validator、Compiler、修复)全部下沉到子图与节点内部,根图不重复实现任何业务(root.py 开头注释明确写了这一约束)。

2.2 一条核心生成链路
一句话需求
  → requirement_research(需求分析 + 领域研究)
  → architecture(skeleton → topology → behavior,产出 ArchitectureIntent)
  → entity(实体设计,产出 EntityDraft)
  → action(设计 → 校验 → Critic 语义审查 → 组装,产出 ActionDraft)
  → experience(体验设计)
  → presentation(界面设计)
  → integration(整合 + verify)
  → Blueprint(可运行系统蓝图,落库不可变)

每个子图内部又是一个「小状态图」,例如 action 子图的拓扑是:

design → validate → critic → assemble
                ↘        ↗
                revise(定向修复)
2.3 契约驱动与产物管线

生成链路里流动的不是"一段随意的 JSON",而是强类型的 Artifact。每个 Artifact 有稳定的 logical_key(如 entity.draft:book)、revision(单调递增)、content_hash(内容指纹)。节点之间通过 ArtifactRef 引用传递,而不是把整份产物塞进状态。

三、核心技术点深度剖析

3.1 智能体编排引擎:LangGraph 状态图 + Command 显式路由

编排引擎是整个平台的地基。它的核心难题是:7 个子图、几十个 LLM 节点,如何在"可暂停、可恢复、可修复"的前提下,把"产物正确性"牢牢锁住?

3.1.1 整体方案:根图只存引用,子图负责业务

根图的状态契约非常克制——checkpoint 里只允许 ArtifactRef、FailureEnvelope、RepairTask 和计数器,禁止保存完整 JSON、LLM 实例、数据库连接:

# backend/app/generation_graph/root.py (L98-L125)
class RootGenerationState(TypedDict, total=False):
    """The ONLY state that enters the root LangGraph Checkpoint.
    ...
    Forbidden contents:
      - ArchitecturePlan / Entity/Action Draft complete JSON
      - raw_requirement
      - API key
      - database Connection
      - LangChain Model / Tool instances
      - GenerationRun/Step/Event copies
    """
    run_id: str
    artifact_heads: Annotated[dict[str, ArtifactRef], merge_artifact_heads]
    branch_failures: Annotated[dict[str, Any], merge_branch_failures]
    pending_repair: Annotated[RepairTask | None, _take_right]
    terminal_status: Annotated[str, _take_right]
    repair_counters: Annotated[dict[str, int], merge_repair_counters]
    pending_revalidations: Annotated[dict[str, RepairTask], merge_repair_tasks]

为什么状态只放 ArtifactRef? 有三个原因:

  1. Checkpoint 体积可控:LLM 产出一份 ArchitectureIntent 可能几千 token,如果塞进状态,每次 checkpoint 都要全量序列化,run 一多就爆。
  2. 可重放:产物本身落库(ArtifactStore),状态里只存"指向哪个修订的引用"。恢复时按引用重新加载,绝不依赖内存里的残留数据。
  3. 强制边界:状态里没有 LLM 实例、没有数据库连接,从根本上杜绝"在状态里塞不可序列化对象"这种坑。
3.1.2 Command(goto=…) 显式路由,不用普通串行边

子图内部的路由全部用 Command(goto=...) 显式表达,而不是靠 LangGraph 的默认串行边。例如 action 子图:

# backend/app/generation_graph/subgraphs/action.py
branch_builder.add_node(_DESIGN_NODE, make_design_node(executor, design_handler))
branch_builder.add_node(_VALIDATE_NODE, make_validate_node(executor, validate_handler, runtime))
branch_builder.add_node(_CRITIC_NODE, make_critic_node(executor, critic_handler))
branch_builder.add_node(_ASSEMBLE_NODE, make_assemble_node(executor, assemble_handler, runtime))
branch_builder.add_node(_REVISE_NODE, make_revise_node(executor, revise_handler))

节点执行结束后,由 NodeExecutionOutcome.to_command() 把「成功 / 失败 / 取消」翻译成 LangGraph 的 Command,显式 goto 到下一个节点或 END:

# backend/app/generation_graph/node_executor.py (L626-L656)
# NodeExecutionOutcome.to_command() 将成功/失败/取消状态和 goto
# 转换为 LangGraph Command,用于显式路由到 repair 节点或 END。

为什么显式路由而不是串行边? 因为同一个节点可能有多种出口(成功 → 下一步;校验失败 → 定向修复;框架错误 → 终止),串行边无法表达这种"一张图多出口"的拓扑,而 Command(goto=...) 可以在节点内部根据结果动态决定下一跳——修复循环、分支汇合、提前终止都靠它表达。

3.1.3 标量字段用 last-write-wins reducer

terminal_statuspending_repair 这类标量字段,用 _take_right(last-write-wins)作为 reducer,避免 Command 返回同时携带 state_update 和状态覆盖时触发 LangGraph last_value channel 的"每步只能写一次"守卫:

# backend/app/generation_graph/root.py (L81-L90)
def _take_right(left: Any, right: Any) -> Any:
    """Last-write-wins reducer for scalar state fields..."""
    return right
3.2 失败恢复与意图修复

LLM 应用最头疼的就是不确定性与失败。sysgen-platform 把失败分成明确的三级,并设计了"定向修复"机制——这是它与"失败就重跑"朴素方案最大的分水岭。

3.2.1 三级失败模型
级别示例处理
校验失败产物不符合契约(字段缺失、类型错)RepairTask 定向修复,不重跑全阶段
工具失败web_search 超时、MCP 不可用节点内兜底/重试,不升级为整体失败
框架错误OOM、DB 断连graph_invocation_failed 标记 run 失败

失败会封装成 FailureEnvelope,由 FailureRouter 决定:能修复的进 pending_repair,不能修复的直接终止

3.2.2 定向补丁修复 vs 重跑阶段

RepairTask 是"意图修复"的结构化入口,它携带:触发修复的 blocker、修复动作、当前 head 的引用(base_artifact_ref)、以及允许/禁止修改的路径:

# backend/app/generation/agent_contracts.py (L105-L136)
class RepairTask(BaseModel):
    ...
    base_artifact_ref: ArtifactRef   # 当前 head 引用,修复必须基于它
    allowed_paths: list[str]         # 允许修改的路径
    forbidden_paths: list[str]       # 禁止修改的路径(防止误伤已正确产物)

为什么定向修复而不是重跑? 一个典型的场景:架构阶段产物通过了,action 阶段某个动作校验失败。此时正确的做法是只修补那个 action,而不是把 architecture / entity 全部重跑一遍——重跑会引入 LLM 非确定性,可能把已经正确的产物改坏,且浪费大量 token 和时间。修复是"打补丁",不是"重写"。

3.2.3 base_artifact_ref 失配即终止

修复任务携带的 base_artifact_ref 是对"我在哪个版本上修复"的断言。如果修复任务到达时,目标产物的 head 已经变了(base_artifact_ref 失配),说明这个修复任务已经过期——必须立即终止,不能拿着旧补丁去改新产物,否则会引入不一致。

3.2.4 单实例 Leader + DB 租约

LLM 生成天然非确定,如果两个进程同时消费同一个 run,结果会互相打架。sysgen-platform 用 PostgreSQL 会话级 advisory lock 保证"全平台只有一个权威消费进程":

# backend/app/generation_graph/integration/run_launcher.py (L136-L173)
def _try_acquire_leader_lock(self) -> bool:
    """Acquire the process-wide PostgreSQL launcher lock.
    The connection is intentionally kept open for the lifetime of the
    launcher because advisory locks are session-scoped..."""
    conn = self._engine.connect()
    acquired = bool(
        conn.execute(
            select(func.pg_try_advisory_lock(LAUNCHER_ADVISORY_LOCK_KEY))
        ).scalar_one()
    )
    ...
    self._leader_connection = conn   # 保持连接不释放 = 持锁
    return True

为什么用 advisory lock 而不是 Redis SETNX? 因为 run 的租约、checkpoint 本来就在 PostgreSQL 里,用同一个数据库的 advisory lock 可以和事务强一致(会话级锁随连接生命周期自动释放,进程崩溃也不残留),少一个分布式组件。拿到锁的 launcher 用 claim_next 原子认领 run + 续租(heartbeat),模型调用期间也不丢租约。

3.3 RAG 检索链路

sysgen-platform 不是"只靠模型瞎编",而是先做领域研究再设计系统——这保证了生成结果的行业可信度。

3.3.1 research 子图编排

requirement_research 子图内部是三步走:

requirement.analyze(需求分析 → RequirementBrief)
  → research.plan(LLM 生成检索计划 ResearchQueryPlan)
  → research.execute(多工具并行检索 → EvidenceBundle)
  → research.synthesize(证据融合 → DomainResearchBrief)

ResearchQueryPlan 是 LLM planner 输出的检索计划,包含 research_objective / topics / queries

# backend/app/generation/research_contracts.py (L206-L219)
class ResearchQueryPlan(BaseModel):
    research_objective: str          # 本次检索的目标
    topics: list[str]                # 要调研的行业主题
    queries: list[ResearchQuery]     # 具体的检索查询(含证据需求)
3.3.2 两个检索工具:web_search 与 rag_search
  • web_search(Firecrawl):网页检索。关键设计是返回不透明 result_id,真实 URL 只写入 per-run 的 store,模型看不到真实 URL——防止模型伪造 URL 或越界访问。
  • rag_search(Zilliz/Milvus):知识库混合检索,Dense + BM25 → RRF 融合 → Rerank,结果统一规范化为带 provenance 的 EvidenceCandidate
# backend/app/generation_graph/tools/rag_search.py (L1-L15)
# rag_search 工具:包装现有 hybrid retrieval pipeline
#(Dense + BM25 → RRF → Rerank),使用 KnowledgeRetriever.search,
# 将检索结果标准化为 EvidenceCandidate,是 RAG 检索链路核心实现入口。
3.3.3 DomainResearchBrief:研究简报的结构

检索完成后由 research_synthesizer 融合成一份结构化简报,供 SystemArchitect 使用:

# backend/app/generation/research_contracts.py (L335-L397, L449-L469)
class DomainResearchBrief(BaseModel):
    research_quality: ...            # 研究质量自评
    domain_summary: ...              # 领域总结
    terminology: list[...]           # 行业术语
    business_concepts: list[...]     # 业务概念(候选实体来源)
    operational_records: list[...]   # 经营/业务记录(候选"记录实体"来源)
    relationship_patterns: list[...] # 关系模式

关键约束(写在 system_architect 提示词里):研究简报是行业证据,不是设计蓝图——SystemArchitect 必须结合用户需求筛选重组,不能照抄 business_concepts 当实体列表。这防止了"模型被研究简报带偏"。

3.3.4 证据融合与可观测性

每个节点都有详细日志(进入/退出、读取的 artifact 修订、检索命中的证据数),配合 LangSmith trace,能在人工测试时观察整条检索链路的每一步。RAG 检索结果带 provenance(来源可追溯),供后续证据融合和审查使用。

3.4 MCP 集成与契约体系
3.4.1 契约注册表:一切语义 code 必须登记

平台里所有 Validator / Compiler / Critic 能产生的语义审查 code(如 ARCH_FORBIDDEN_TERMSACTION_*),都必须注册到 contract_registry.py,并映射到"由谁负责修复"(repair owner)。这是可修复性的前提:审查发现的问题,系统才能知道找谁、怎么修

# backend/app/generation/contract_registry.py (L401-L458)
# 所有 Validator/Compiler/Critic 可产生的 invariant code 必须在此注册,
# 并映射 repair owner(由哪个 handler 负责修复)。
3.4.2 MCP:默认禁用 + 身份隐藏 + 写工具黑名单

MCP 是双刃剑:接入工具增强能力,但也带来数据泄露和误操作风险。sysgen-platform 的处理层层设防:

  1. 默认禁用MCP_ENABLED 默认 false,只有显式配置才启用(config_loader.py)。
  2. 身份隐藏:MCP 工具注册进生成 Agent 时,只暴露 ToolCapability 语义(能力、证据类型、source quality、read_only),不暴露真实 server/tool 名与凭据mcp/registry.py)。
  3. 写工具黑名单create_/update_/delete_/write_/execute_/insert_/... 前缀的工具被 is_write_tool() 拦截,而且在多个层面拦截(不暴露给模型、注册时检查、执行时二次拒绝):
# backend/app/generation_graph/tools/mcp_tools.py (L36-L65)
# MCP 写工具黑名单:create_/update_/delete_/write_/execute_/...
# 等前缀会被 is_write_tool() 拦截(多层:不暴露 + 注册检查 + 执行拒绝)。

为什么这么严? 生成平台里的 LLM 是"只读的设计者",它只负责"读资料、出设计",绝不该有写入外部系统(发邮件、建工单、改数据库)的能力。黑名单 + 隐藏身份,是把 LLM 的能力边界锁死在"只读"上。

3.4.3 JSON 模式与 LLM 端点稳定性

这一节全是实战踩坑换来的经验(详见第四节)。简单说三条铁律:

  1. LLM_API_BASE 必须用 DashScope 兼容端点 https://dashscope.aliyuncs.com/compatible-mode/v1,不能走 ws-* MAAS 端点(后者返回非标准 choices:null + text,导致结构化解析概率性失败)。
  2. 纯生成 role 走 json_modeJSON_MODE_ROLES = architecture_critic, action_critic, experience_designer),避免 tool 策略下 qwen 发 tool_calls 无响应。
  3. 必填数组字段要做 validator 兜底——json_mode 下 LLM 概率性把数组输出成 null 或裸字符串,需要在契约里 mode="before" 规范化为数组:
# backend/app/generation/critique_contracts.py (L138-L156)
# CritiqueIssue.semantic_addresses / basis 等数组字段:
# mode="before" validator 把 None/"" 规范化为 []、裸字符串包成单元素数组。
3.4.4 提示词体系:分阶段 + 分级审查
  • SystemArchitect 三阶段生成:skeleton(实体骨架)→ topology(关系拓扑)→ behavior(行为),每阶段只输出该阶段该管的内容,避免一次输出太多导致 JSON 畸形。
  • 分级 Criticarchitecture_critic 审架构语义、action_critic 审动作语义(业务效果 / 目标实体 / primary_transition 一致性、principal/dependent 方向、delete_record 不支持等),审查意见带着 semantic_addresses 精确定位问题点,方便修复任务定向打补丁。
  • prompt_policy_version:提示词版本号纳入契约 manifest,改提示词会反映到产物指纹上(保证同一次 run 内行为一致)。
3.5 产物不可变与级联失效
3.5.1 content_hash 由存储层计算,防伪造

ArtifactWrite 规定:content_hash 由 ArtifactStore 自己算,调用方不能伪造。提交方只给 payload 和它读取的依赖,存储层校验、规范化 JSON、算 SHA-256、分配 revision,一个事务内完成"插入不可变修订 + 依赖边 + head 推进":

# backend/app/generation_runtime/artifact_records.py (L103-L156)
class ArtifactWrite(BaseModel, Generic[T]):
    """One artifact a node wants to commit...
      2. computes content_hash itself (callers cannot forge it),
      3. assigns revision from the head CAS,
      4. inserts the immutable revision row + dependency edges + head
         advance in one transaction."""
    logical_key: str          # 稳定语义地址,如 'entity.draft:supplier'
    payload: T                # 业务内容(Pydantic 模型)
    contract_manifest: ContractManifest   # 契约指纹
    input_fingerprint: str    # 任务+依赖+契约+模型配置的稳定哈希
    dependencies: list[DependencyRead]    # 读过的上游精确修订
3.5.2 级联失效:input_fingerprint 驱动的"精确失效"

下游产物的 input_fingerprint 包含上游 content_hash。当上游产物变化,下游的输入指纹对不上,就知道"我基于的那个上游版本没了"——于是下游 head 失效、需要基于新版本重生成。这就是级联失效:不用人工去判断谁受影响,指纹自动暴露断链:

# backend/app/generation_runtime/artifact_records.py (L55-L95)
class DependencyRead(BaseModel):
    # 下游依赖上游产物的精确版本与 provider_content_hash,
    # 携带 on_change 策略:上游变化导致下游失效/重生成或重校验。

为什么"不可变 + 指纹"而不是"直接改库"? 这是全项目最重要的一条硬约束:Artifact 不可变,任何"直接改产物记录"的操作都会让下游的引用失去锚点,产生幽灵不一致。要改,就新写一个修订,让指纹去驱动级联——用机制保证一致性,而不是靠人记着"我改过哪"

3.5.3 从生成到可运行蓝图

生成的终点是 BlueprintPackageCandidate——把 system / entity / semantic 各类节点合并、提取 physical edges,形成统一的 nodes / edges / source_artifacts 图,并算出稳定的 content_hash

# backend/app/generation/blueprint_contracts.py (L32-L137)
class BlueprintPackageCandidate(BaseModel):
    content_hash: str          # 确定性构建、不可变快照(排序 JSON 计算稳定哈希)
    input_fingerprint: str
    nodes: list[...]           # blueprint_nodes
    edges: list[...]           # blueprint_edges
    source_artifacts: ...      # 溯源:由哪些 Artifact 组装而来

蓝图一旦落库就是不可变快照,平台端基于它渲染系统列表(core_modulesdomain:* 节点提取),租户端基于它运行业务系统。

四、踩坑复盘与工程经验

这一节是"代码都能跑,但要跑到稳定"的真实代价,全部亲历:

  1. json_mode 数组字段输出 null / 裸字符串
    qwen 在 json_mode 下对必填数组字段(binding_keys、semantic_addresses、effect_keys 等)概率性输出 null"",导致契约校验失败(OUTPUT_SCHEMA_INVALID)。修复:契约模型加 mode="before" validator 把 None/“” 规范化为 []、裸字符串包成单元素数组,并在 prompt 里明确"禁止 null/空字符串"。

  2. CapabilityProbe 是伪探测
    它只构造 ToolStrategy/ProviderStrategy 验证对象可用,不实际调用 LLM,因此对 qwen-max 恒返回 tool。运行时纯生成 role 走 tool 策略,qwen 概率性发 tool_calls 但无 tool message 响应 → 报 400 或 ModelCallLimitExceededError(重试耗尽,底层错误被掩盖)。修复:把纯生成 role(architecture_critic / action_critic / experience_designer)对齐 json_mode。

  3. 复杂 schema 不能无脑走 json_mode
    把 system_architect 加进 json_mode 后,无 schema 约束导致输出畸形 JSON(数组里混入 '(' / ')'、entity_keys 变裸字符串)。结论:简单 schema 走 json_mode,复杂 schema 保持 tool 策略,逐个验证,不能一刀切。

  4. LLM 端点选错 → 概率性解析失败
    ws-* MAAS 端点返回非标准 choices:null + text,让 langchain 绕过结构化解析,research.plan 等阶段概率性 OUTPUT_SCHEMA_INVALID。修复:统一用 DashScope 兼容端点 + LLM_SUPPORTS_JSON_SCHEMA=true

  5. git reset --hard 会无差别覆盖所有已跟踪文件
    一次回退让之前未提交的 8-10 个关键文件(如 ActionCritic 语义审核层)全部丢失且无法恢复。教训:大动作前先 commit / stash,回退前先看清楚。

  6. 小内存服务器上前台硬跑 docker build 会拖垮整机
    2 核 1GB 的服务器上 pip 装 112 个依赖包,前台 docker compose build 会耗尽 CPU+内存,系统陷入 swap 抖动、SSH 全断。修复:nohup ... > build.log 2>&1 & 后台跑 + 轮询日志;Dockerfile 分层(先装依赖、后复制代码,--no-deps 重链),以后改代码重建只重装当前包。

  7. 提示词与历史版本对齐靠 trace 反推
    git 回退丢了提示词,靠历史成功的 trace(langsmith)反推 8-10 版本的 system_architect / action_critic 提示词逐字比对修正——证明"可观测性投入"在最坏情况下是救命稻草。

结语

sysgen-platform 最大的价值,不是"能生成系统"这个结果,而是它把 LLM 应用从"脚本式 demo"推到了"可恢复、可修复、可追溯的工程系统"。核心三句话:

  • 状态只存引用,产物落库不可变 → 可重放、可恢复、不会脏;
  • 一切失败可分类、可定向修复 → 不靠重跑赌运气;
  • 一切能力可约束(MCP 黑名单、契约注册表、JSON 模式) → LLM 的破坏力被锁死,创造力被释放。

如果你也在做多阶段 LLM 编排,希望这篇复盘里的"为什么这样设计"能帮你少走几条我们踩过的弯路。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐