从一个"不可能的需求"说起
去年底,团队接到一个需求:用 AI 自动生成建筑设计领域的专业文档。
不是那种几百字的摘要,而是动辄 50 页、必须引用行业标准、每个章节都有合规要求的正式文档。
第一反应是:这事儿靠 ChatGPT 套个壳能搞定吗?
试了一下,答案是不能。单次对话生成的内容质量不稳定,长文档的上下文窗口直接爆掉,更别提还要精准检索特定行业标准了。
于是我们决定自己造------OpenSpec,一个基于 LangGraph 多智能体架构的 AI 长文档生成平台。项目已开源,这篇文章聊聊我们在工程落地过程中踩过的真实的坑。
整体架构:Nginx 分流,双引擎并行
很多 AI 应用的架构是"前端 → 后端 → AI 服务"的串联模式。我们一开始也是这么设计的,但很快发现了问题:SSE 流式输出经过 Java 后端转发后,延迟明显增大,而且 Spring Boot 处理长连接并不是它的强项。
最终我们采用了 Nginx 分流的双引擎架构:
bash
┌─ /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 编排了一个多节点状态图:
javascript
Router(意图路由)
├── 文档生成链路:
│ Researcher(知识库检索 + 上下文收集)
│ ↓
│ Generator(章节内容生成)
│ ↓
│ Auditor(质量审核)→ 不合格 → 回到 Researcher 重新检索
│ → 合格 → 输出最终内容
│
└── General Agent(通用对话)
为什么不用单 Agent?
单 Agent 生成专业文档有一个致命问题:它会"自说自话"。生成的内容看起来通顺,但可能引用了不存在的标准条款,或者遗漏了关键的合规要求。
拆成三个角色后,职责清晰:
- Researcher:只负责从知识库检索相关标准和案例,不做内容生成
- Generator:基于 Researcher 提供的上下文生成章节内容
- Auditor:审核生成内容是否符合要求,不合格则打回重来
关键的循环控制参数:
python
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 调用前,先算清楚"模型还剩多少空间给检索上下文":
python
def calculate_available_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 # 响应预留
)
# 剩余空间才是可用的检索上下文窗口
available = counter.get_token_limit() - fixed_tokens
return max(available, 500)
几个关键策略:
- ToolMessage 裁剪:上下文最多保留 30 条 ToolMessage,超出的按时间顺序丢弃
- 中文 Token 估算优化:Qwen 模型的 tokenizer 对中文的切分和 tiktoken 有差异,我们做了专门的系数校准(中文字符 ×1.2,英文单词 ×1.3)
- Langfuse 成本追踪:每次 LLM 调用都记录 input/output token 数和对应成本,方便按项目、按章节分析费用
实测效果:上下文长度压缩约 70%,长文档生成全程不会触发 Token 超限。
踩坑 2:RAG 检索质量------召回率不稳定怎么办
直接用 RAG 检索行业标准,召回率忽高忽低。有时候明明知识库里有相关内容,就是检索不出来。
我们的方案分三层:
第一层:知识库分类。案例库和标准库分开存储、分开检索,避免不同类型的文档互相干扰。
python
DEFAULT_CASE_KB_IDS = [...] # 案例库------过往项目的文档范例
DEFAULT_STAND_KB_IDS = [...] # 标准库------行业规范、国标等
第二层:相似度阈值 + 最小内容阈值。
python
# 相似度低于 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 的版本管理和运行时加载:
python
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 + Java 17 + MyBatis Plus |
| AI 引擎 | FastAPI + LangGraph + LangChain + DashScope (Qwen) |
| 知识库 | RAGFlow |
| 可观测性 | Langfuse(Prompt 管理 + 调用追踪 + 成本分析) |
| 存储 | PostgreSQL(业务数据 + LangGraph checkpoint) |
| 部署 | Docker Compose,4 个容器,Nginx 做路由分发 |
写在最后
做 AI 长文档生成这大半年,最大的感受是:核心难点不在模型能力,而在工程架构。
选什么模型、用什么框架,这些决策一两天就能定下来。但 Token 怎么省、检索质量怎么稳定、Prompt 怎么高效迭代、流式输出怎么做到丝滑------这些工程问题,每一个都需要反复试错和打磨。
没有银弹,都是一个个方案堆出来的。
项目已开源:github.com/zhuzhaoyun/... ,欢迎 Star 和 PR。如果你也在做类似的 AI 文档生成系统,欢迎交流。