从一个"不可能的需求"说起
去年底,团队接到一个需求:用 AI 自动生成建筑设计领域的专业文档。

不是那种几百字的简短摘要,而是动辄五六十页、必须引用行业标准、每个章节都有严格的合规要求的正式文档。
第一反应是:这事儿靠 ChatGPT 套个壳能搞定吗?
实践下来,答案是不能。单次对话生成的内容质量不稳定,长文档的上下文窗口直接爆掉,更别提精准检索特定行业标准这种硬需求了。
于是我们决定自己造——OpenSpec,一个基于 LangGraph 多智能体架构的 AI 长文档生成平台。项目已开源,这篇文章就聊聊我们在工程落地过程中踩过的那些真实坑。
整体架构:Nginx 分流,双引擎并行
很多 AI 应用的架构是“前端 → 后端 → AI 服务”的串联模式。我们一开始也是这么想的,但很快就发现了问题:SSE 流式输出经过 Ja va 后端转发后,延迟明显增大,而且 Spring Boot 处理长连接确实不是它的强项。
最终我们采用了 Nginx 分流的双引擎架构:
┌─ /api/*──→ Spring Boot(业务处理)用户 → Vue3 前端 → Nginx ─┤└─ /agent/* ──→ FastAPI AI 引擎(文档生成) ├── LangGraph(工作流编排) ├── RAGFlow(知识库检索) ├── DashScope/Qwen(LLM) └── Langfuse(可观测性)
两条链路各司其职:
- Spring Boot 负责文档管理、用户管理、项目信息等业务 CRUD,数据存 PostgreSQL
- FastAPI Agent 负责所有 AI 相关的工作——工作流编排、RAG 检索、LLM 调用、流式输出
- Nginx 做反向袋里和路由分发,前端根据接口路径自动走不同的后端
这么做的好处很明显:AI 生成的 SSE 流直接从 FastAPI 推到前端,中间没有额外的转发层,延迟降到了最低;同时业务逻辑和 AI 逻辑完全解耦,两边可以独立迭代、独立部署。
整个系统打包成四个 Docker 容器:Web(Nginx + Vue)、Backend(Spring Boot)、Agent(FastAPI)、PostgreSQL。一条 docker-compose up 命令就能一键启动。
为什么选 LangGraph
2026 年的多智能体编排框架选择不少,LangGraph、CrewAI、AutoGen 各有各的定位。最终选定 LangGraph 的核心原因有几点:
首先,确定性边和护栏。专业文档生成不能“随机发挥”,每一步都需要可控、可追踪、可回溯。LangGraph 的有状态图(Stateful Graph)天然支持这一点——节点之间的流转是确定性的条件分支,而不是 Agent 自由发挥。
其次,持久化检查点。50 页文档不是一次生成的,是按章节逐个生成的。每个章节生成完,状态会写入 PostgreSQL 作为 checkpoint。如果中途出错,可以从断点恢复,不用从头再来。
最后,原生流式支持。LangGraph 的 astream_events 提供了 token 级别的流式输出能力,配合 SSE 推送到前端,用户能实时看到生成过程。
多智能体工作流:写、查、审分离
这是整个系统最核心的设计。我们用 LangGraph 编排了一个多节点状态图:
Router(意图路由)├── 文档生成链路:│ Researcher(知识库检索 + 上下文收集)│ ↓│ Generator(章节内容生成)│ ↓│ Auditor(质量审核)→ 不合格 → 回到 Researcher 重新检索│ → 合格 → 输出最终内容│└── General Agent(通用对话)
为什么不用单 Agent?
单 Agent 生成专业文档有一个致命问题:它会“自说自话”。生成的内容看起来通顺,但可能引用了不存在的标准条款,或者遗漏了关键的合规要求。
拆成三个角色后,职责变得非常清晰:
- Researcher:只负责从知识库检索相关标准和案例,不做内容生成
- Generator:基于 Researcher 提供的上下文生成章节内容
- Auditor:审核生成内容是否符合要求,不合格则打回重来
关键的循环控制参数:
MAX_RESEARCH_LOOPS = 3 # Researcher 最多循环 3 次MAX_AUDIT_LOOPS = 2 # Auditor 最多审核 2 次MAX_AUDITOR_TOOL_CALLS = 5 # Auditor 单次工具调用上限
这些是防止无限循环烧 Token 的护栏。从实际测试来看,大部分章节 1-2 轮就能通过审核。
上下文窗口管理:长文档场景下如何避免 Token 超限
生成 50 页文档,如果把所有已生成章节都塞进上下文,很容易超出模型的最大输入长度。在长文档场景下,上下文窗口管理是绕不开的工程问题。
我们的方案是动态 Token 预算——每次 LLM 调用前,先算清楚“模型还剩多少空间给检索上下文”:
def calculate_a vailable_tokens(question, template, project_info):counter = get_token_counter()# 先算固定开销:问题 + 模板 + 项目信息 + Prompt + 响应预留fixed_tokens = (counter.count_tokens(question) +counter.count_tokens(template) +counter.count_tokens(project_info) +1000 +# Prompt 模板开销2000 # 响应预留)# 剩余空间才是可用的检索上下文窗口a vailable = counter.get_token_limit() - fixed_tokensreturn max(a vailable, 500)
几个关键策略:
- ToolMessage 裁剪:上下文最多保留 30 条 ToolMessage,超出的按时间顺序丢弃
- 中文 Token 估算优化:Qwen 模型的 tokenizer 对中文的切分与 tiktoken 有差异,我们做了专门的系数校准(中文字符 ×1.2,英文单词 ×1.3)
- Langfuse 成本追踪:每次 LLM 调用都记录 input/output token 数和对应成本,方便按项目、按章节分析费用
实测效果:上下文长度压缩约 70%,长文档生成全程不会触发 Token 超限。
踩坑 2:RAG 检索质量——召回率不稳定怎么办
直接用 RAG 检索行业标准,召回率忽高忽低。有时候明明知识库里有相关内容,就是检索不出来。
我们的方案分三层:
第一层:知识库分类。案例库和标准库分开存储、分开检索,避免不同类型的文档互相干扰。
DEFAULT_CASE_KB_IDS = [...]# 案例库——过往项目的文档范例DEFAULT_STAND_KB_IDS = [...] # 标准库——行业规范、国标等
第二层:相似度阈值 + 最小内容阈值。
# 相似度低于 0.55 的结果直接过滤chunks = rag_object.retrieve(question=query,dataset_ids=kb_ids,similarity_threshold=0.55)# 检索内容总量低于 1000 字符时,触发二次检索(换个角度重新查)MIN_CONTENT_THRESHOLD = 1000
第三层:个人模板知识库。用户可以上传自己的文档模板,系统会优先匹配用户模板的结构和风格,生成的文档更贴合用户的实际需求。
踩坑 3:Prompt 管理——硬编码是条死路
早期 Prompt 是硬编码在 Python 代码里的,每次调整都要改代码、重新构建 Docker 镜像、重新部署。
做过 AI 应用的都清楚,Prompt 调优是个高频操作,一天改十几次很正常。这个流程直接把迭代效率拖死了。
现在全部迁移到 Langfuse,用它做 Prompt 的版本管理和运行时加载:
class PromptManager:"""Langfuse Prompt 管理器(单例模式)"""def get_prompt(self, prompt_name, label=None):target_label = label or self.default_label# 默认 "latest"return self.langfuse.get_prompt(prompt_name, label=target_label)# 运行时动态加载,支持变量编译prompt = prompt_manager.get_prompt("construction_agent_system")full_prompt = prompt.compile(context=context,question=question,template=template)
改完 Prompt 在 Langfuse 后台保存,线上立即生效,不用重新部署。每个版本自动保存历史记录,出了问题随时能回滚。
更关键的是,Langfuse 提供了完整的调用链路追踪——每次 LLM 调用关联到具体的 Prompt 版本、输入输出、Token 消耗,排查生成质量问题时能精确定位到是哪个 Prompt 版本导致的。
流式输出:让用户看到生成过程
长文档按章节生成,单个章节的生成时间从几秒到几十秒不等。实时反馈生成进度,对用户体验至关重要。
我们用的是 SSE(Server-Sent Events)做流式推送。前端直连 FastAPI Agent 服务(通过 Nginx 的 /agent/ 路由),LangGraph 的 astream_events 把每个 token 实时推到前端,逐字渲染。
除了 token 流,我们还推送了工作流进度事件——用户能看到当前处于哪个阶段(“正在检索资料”、“正在生成内容”、“正在审核”),而不是只看到文字在跳动。这个细节对用户体验的提升,比想象中大得多。
技术栈一览
| 层级 | 技术 |
|---|---|
| 前端 | Vue 3 + TypeScript + Vite + Element Plus |
| 业务后端 | Spring Boot 3 + Ja va 17 + MyBatis Plus |
| AI 引擎 | FastAPI + LangGraph + LangChain + DashScope (Qwen) |
| 知识库 | RAGFlow |
| 可观测性 | Langfuse(Prompt 管理 + 调用追踪 + 成本分析) |
| 存储 | PostgreSQL(业务数据 + LangGraph checkpoint) |
| 部署 | Docker Compose,4 个容器,Nginx 做路由分发 |
写在最后
做 AI 长文档生成这大半年,最大的感受是:核心难点不在模型能力,而在工程架构。
选什么模型、用什么框架,这些决策一两天就能定下来。但 Token 怎么省、检索质量怎么保证稳定、Prompt 怎么高效迭代、流式输出怎么做到丝滑——这些工程问题,每一个都需要反复试错和打磨。
没有什么银弹,都是一个个方案堆出来的。
