一个 AI 智能体生成平台(sysgen-platform)的技术深度复盘
本文以一个真实项目「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 / Zilliz | RAG 检索:Dense 向量 + BM25 稀疏 → RRF 融合 → Rerank 重排 |
| LLM | DashScope(qwen-max / qwen-plus) | 全部节点统一 qwen-max;兼容 OpenAI 协议端点 |
| 网页检索 | Firecrawl | web_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 架构分层
根图只做一件事:把 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? 有三个原因:
- Checkpoint 体积可控:LLM 产出一份 ArchitectureIntent 可能几千 token,如果塞进状态,每次 checkpoint 都要全量序列化,run 一多就爆。
- 可重放:产物本身落库(ArtifactStore),状态里只存"指向哪个修订的引用"。恢复时按引用重新加载,绝不依赖内存里的残留数据。
- 强制边界:状态里没有 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_status、pending_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_TERMS、ACTION_*),都必须注册到 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 的处理层层设防:
- 默认禁用:
MCP_ENABLED默认false,只有显式配置才启用(config_loader.py)。 - 身份隐藏:MCP 工具注册进生成 Agent 时,只暴露
ToolCapability语义(能力、证据类型、source quality、read_only),不暴露真实 server/tool 名与凭据(mcp/registry.py)。 - 写工具黑名单:
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 端点稳定性
这一节全是实战踩坑换来的经验(详见第四节)。简单说三条铁律:
- LLM_API_BASE 必须用 DashScope 兼容端点
https://dashscope.aliyuncs.com/compatible-mode/v1,不能走ws-*MAAS 端点(后者返回非标准choices:null + text,导致结构化解析概率性失败)。 - 纯生成 role 走 json_mode(
JSON_MODE_ROLES = architecture_critic, action_critic, experience_designer),避免 tool 策略下 qwen 发tool_calls无响应。 - 必填数组字段要做 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 畸形。
- 分级 Critic:
architecture_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_modules 从 domain:* 节点提取),租户端基于它运行业务系统。
四、踩坑复盘与工程经验
这一节是"代码都能跑,但要跑到稳定"的真实代价,全部亲历:
-
json_mode 数组字段输出 null / 裸字符串
qwen 在 json_mode 下对必填数组字段(binding_keys、semantic_addresses、effect_keys 等)概率性输出null或"",导致契约校验失败(OUTPUT_SCHEMA_INVALID)。修复:契约模型加mode="before"validator 把 None/“” 规范化为[]、裸字符串包成单元素数组,并在 prompt 里明确"禁止 null/空字符串"。 -
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。 -
复杂 schema 不能无脑走 json_mode
把 system_architect 加进 json_mode 后,无 schema 约束导致输出畸形 JSON(数组里混入'('/')'、entity_keys 变裸字符串)。结论:简单 schema 走 json_mode,复杂 schema 保持 tool 策略,逐个验证,不能一刀切。 -
LLM 端点选错 → 概率性解析失败
ws-*MAAS 端点返回非标准choices:null + text,让 langchain 绕过结构化解析,research.plan 等阶段概率性OUTPUT_SCHEMA_INVALID。修复:统一用 DashScope 兼容端点 +LLM_SUPPORTS_JSON_SCHEMA=true。 -
git reset --hard会无差别覆盖所有已跟踪文件
一次回退让之前未提交的 8-10 个关键文件(如 ActionCritic 语义审核层)全部丢失且无法恢复。教训:大动作前先 commit / stash,回退前先看清楚。 -
小内存服务器上前台硬跑 docker build 会拖垮整机
2 核 1GB 的服务器上 pip 装 112 个依赖包,前台docker compose build会耗尽 CPU+内存,系统陷入 swap 抖动、SSH 全断。修复:nohup ... > build.log 2>&1 &后台跑 + 轮询日志;Dockerfile 分层(先装依赖、后复制代码,--no-deps重链),以后改代码重建只重装当前包。 -
提示词与历史版本对齐靠 trace 反推
git 回退丢了提示词,靠历史成功的 trace(langsmith)反推 8-10 版本的 system_architect / action_critic 提示词逐字比对修正——证明"可观测性投入"在最坏情况下是救命稻草。
结语
sysgen-platform 最大的价值,不是"能生成系统"这个结果,而是它把 LLM 应用从"脚本式 demo"推到了"可恢复、可修复、可追溯的工程系统"。核心三句话:
- 状态只存引用,产物落库不可变 → 可重放、可恢复、不会脏;
- 一切失败可分类、可定向修复 → 不靠重跑赌运气;
- 一切能力可约束(MCP 黑名单、契约注册表、JSON 模式) → LLM 的破坏力被锁死,创造力被释放。
如果你也在做多阶段 LLM 编排,希望这篇复盘里的"为什么这样设计"能帮你少走几条我们踩过的弯路。
更多推荐


所有评论(0)