超越简单提示:构建 AI Agent
Source: Dev.to
请提供您希望翻译的完整文本内容,我会按照要求保留源链接、格式和代码块,仅翻译正文部分为简体中文。
问题空间
合同审查遵循一个可预测的模式。法律团队收到对方的红线合同后,会根据组织的风险容忍度审查每一项变更,并对每个建议进行接受、拒绝或修改。对单个合同而言,这一过程可能需要数小时。
当我着手实现自动化时,我意识到该问题涉及多个方面,包括但不限于:
- 根据特定指南分析合同
- 生成带有理由的具体文本建议
- 以 Word 跟踪更改的方式应用修改——而不是纯文本替换
- 在文档变动中保持有效——用户在分析运行时编辑合同
需求 #3 和 #4 是大多数工具卡住的地方。它们在聊天界面输出建议,用户需要手动复制‑粘贴并重新格式化。这并不是自动化,只是更高级的 Ctrl + F。
系统架构
┌─────────────────────────────────────────────────────────────────┐
│ Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web App │ │ Word Add‑in │ │ Backend │ │
│ │ (Next.js) │ │ (Office.js) │ │ (FastAPI) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ REST + SSE │ REST + SSE │ │
│ └────────────────────┴────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ Analysis Engine │ │
│ │ ┌───────────────┐ │ │
│ │ │ DSPy + LLM │ │ │
│ │ │ (OpenAI / │ │ │
│ │ │ Mistral) │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
三个核心组件
- Web 仪表盘 – 用于规则管理、分析和管理的 Next.js 应用。
- Word 加载项 – Microsoft Office 插件(React + Office.js),用户实际在此审阅合同。
- 后端 API – 处理分析、LLM 编排和文档处理的 FastAPI 服务。
有趣的工程实现位于 Word 加载项(文档操作)和 后端 API(分析流水线)。
挑战 #1:可变文档问题
会导致朴素实现出错的场景
| 步骤 | 发生了什么 |
|---|---|
| 1️⃣ | 用户上传了一份 50 页的合同。 |
| 2️⃣ | 系统分析第 1‑50 段,并按段落索引存储建议。 |
| 3️⃣ | 等待期间,用户删除了第 12 段。 |
| 4️⃣ | 系统返回:“第 47 段需要修订。” |
| 5️⃣ | 第 47 段现在变成第 46 段 → 建议被应用到了错误的位置。 |
解决方案 – 段落锚点
我实现了一套逻辑,在预处理阶段为每个段落分配 持久 ID。这些 ID 存储在 OOXML 中,能够在以下情况下保持有效:
- 删除相邻段落
- 剪切并粘贴章节
- 接受/拒绝其他已跟踪的更改
在前端,使用 Zustand store 来维护双向映射:
interface ParagraphStore {
/** index → UUID */
indexToPersistentIdMap: Map;
/** UUID → index */
persistentIdToIndexMap: Map;
/** Fallback matching by text */
findAnchorByText(text: string): string | null;
}
当分析结果返回时,它们引用 UUID。store 在应用时解析 当前 的段落索引,而不是在分析时的索引。
挑战 #2:生成 Word 跟踪更改
这是最难的部分。Office.js 并未提供创建跟踪更改的 API;paragraph.insertText() 只会替换文本。要生成真正的红线(删除使用删除线、插入使用彩色标记),必须:
- 在原始文本和建议文本之间生成差异(diff)。
- 将该差异转换为 OOXML 元素(例如
<w:del>、<w:ins>)。 - 通过 Office.js 将这些 OOXML 元素应用到文档中。
基于 Token 的差异比较
字符级别的差异会在 Word 中产生大量垃圾。
T̶h̶e̶A quick b̶r̶o̶w̶n̶red fox
Token 级别的差异要干净得多:
The → A quick brown → red fox
保留段落属性
合同文档在编号、缩进和样式上依赖度很高。直接替换会破坏这些格式。因此,diff‑to‑OOXML 的转换 保留原始段落属性,仅注入跟踪更改的标记。
Challenge #3: Long‑Running Analysis
一份 50 页、包含 30 条剧本规则的合同分析可能需要 2–3 分钟。让 HTTP 请求被阻塞如此之久是不可接受的。
Session‑Based Async Processing
Client Server
│ │
│ POST /analysis/start → {sessionId}
│ │
│←─────────────────────────────│
│ │
│ GET /analysis/status?sessionId → {status, progress}
│ │
│←─────────────────────────────│
│ │
│ GET /analysis/result?sessionId → {suggestions}
│ │
│←─────────────────────────────│
- 客户端发起分析会话并收到一个
sessionId。 - 服务器在后台工作者中运行繁重的 LLM 驱动流水线(例如 Celery、RQ)。
- 客户端轮询状态或接收服务器发送事件(SSE)更新。
- 完成后,客户端获取建议,这些建议引用持久化的段落 ID。
要点
| ✅ 成功之处 | ❌ 难点 |
|---|---|
| 持久化的段落 ID 能在用户编辑后仍然有效。 | Office.js 缺少原生的修订(tracked‑change)API。 |
| 基于 token 级别的差异保持 Word 输出的可读性。 | 将异步结果映射回实时文档。 |
| 基于会话的异步处理保持 UI 响应。 | 处理边缘情况(表格、脚注、页眉)。 |
通过结合 强大的锚定、token 级别差异 → OOXML 转换 和 异步会话处理,我们可以提供真正自动化的合同审查体验,既尊重文档的原始格式,又能容忍用户驱动的变更。
TL;DR
- 为每个段落分配 UUID 并将其存储在 OOXML 中。
- 在 token 级别进行差异比较,将差异转换为修订(tracked‑change)OOXML,并通过 Office.js 注入。
- 异步运行分析,并返回以持久化 ID 为键的结果。
结果是:一个无缝的端到端系统,使法律团队能够在 Word 中直接使用 AI 生成的修订批注审阅合同 无需离开 Word。
基于会话的轮询示例
POST │
─────►│ Create session
{ session_id: "abc123" } │ Start background task
◄─────│
│
GET /sessions/abc123 │
─────►│
{ status: "processing", │
progress: 45% } │
◄─────│
│
... poll every 3 seconds ... │
│
GET /sessions/abc123 │
─────►│
{ status: "complete", │
results: [...] } │
◄─────│
使用内容哈希进行缓存验证
用户经常多次分析同一合同——不同的指南,或在小幅编辑后进行检查。重新分析未更改的内容会浪费时间和 API 成本。
哈希比较可以捕获:
- 相同文件的重新上传
- “再次分析”点击但实际没有更改
- 多个用户分析相同模板
生产环境中的缓存命中率: ~40 % 用于典型的合同审查工作流。
Challenge #4 – Grounding and Hallucination Prevention
法律文件要求精确。若 AI 建议“供应商责任上限为 100 万美元”,而合同实际写的是“50 万美元”,这比完全没有建议更糟糕。
Solution: Use Structured Output with Explicit Citations。
每个建议都必须引用确切的来源文本。这可以捕捉模型是改写而非逐字引用的情况。
分析流水线
┌────────────────────────────────────────────────────────────────┐
│ Redline Analysis Pipeline │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. DOCUMENT INGESTION │
│ ┌─────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ DOCX │────>│ Extract │────>│ Paragraph │ │
│ │ File │ │ OOXML │ │ Anchoring │ │
│ └─────────┘ └─────────────┘ └──────────────┘ │
│ │
│ 2. CONTENT NORMALIZATION │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ OOXML with │────>│ Unified │ │
│ │ Tracked │ │ Markdown │ │
│ │ Changes │ │ (Original + │ │
│ │ │ │ Revised views) │ │
│ └─────────────┘ └─────────────────┘ │
│ │
│ 3. LLM ANALYSIS │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ │────>│ DSPy │────>│ Structured │ │
│ │ Rules │ │ Signatures │ │ Suggestions │ │
│ └─────────────┘ └─────────────┘ └──────────────┘ │
│ │
│ 4. OUTPUT GENERATION │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Suggestions │────>│ Token Diff │────>│ OOXML │ │
│ │ + Rationale │ │ Algorithm │ │ │ │
│ └─────────────┘ └─────────────┘ └──────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
OOXML‑到‑Markdown 转换
进入的合同通常已经包含来自对方谈判的修订痕迹。转换器:
- 解析 OOXML 元素
- 生成两个同步视图:原始(删除内容,无插入)和 修订(插入内容,无删除)
- 保留来自内容控件的段落 ID
这种抽象让 LLM 在干净的 Markdown 上工作,而不是直接处理原始 XML,将转换复杂性封装在转换层中。
结果
| 指标 | 数值 |
|---|---|
| 处理时间(20‑页合同) | 30‑45 秒(取决于规则复杂度) |
| 缓存命中率 | ~40 %(对未更改内容省去重新分析) |
| 幻觉率 | < 5 %(通过验证捕获,未向用户展示) |
| 格式保留率 | 95 %(段落属性保持) |
| 修订痕迹准确度 | 令牌级精度 |
Lessons Learned
- Office.js 功能强大,但文档不足。 OOXML 操作模式不在任何官方指南中;我通过导出文档并读取 XML 进行逆向工程。
- 字符级差异不适用于文档。 必须先进行分词;通用差异库不了解词边界。
- 异步模式比你想象的更重要。 基于会话的轮询听起来简单,但处理边缘情况(浏览器刷新、网络掉线、服务器重启)需要仔细的状态管理。
- 所有内容都要有来源。 大语言模型会自信地引用不存在的文本。验证层可以捕获这些问题,但前提是输出模式强制显式的来源引用。
- 内容哈希是低成本的保险。 SHA‑256 计算相对于 LLM 成本可以忽略不计;缓存验证在第一周就收回了成本。
技术栈概览
| 层 | 技术 | 为什么 |
|---|---|---|
| 后端 API | FastAPI (Python) | 异步原生,适合长时间运行的任务 |
| LLM 编排 | DSPy | 结构化输出,供应商无关 |
| LLM 提供商 | OpenAI, Mistral | 冗余,成本优化 |
| 数据库 | Supabase (PostgreSQL) | 实时订阅,托管 |
| Web 前端 | Next.js | 用于仪表盘的服务器端渲染,API 路由 |
# Word Add‑in
**Technology:** React + Office.js
*Only option for Word integration*
文档处理
- 工具:
python-docx,自定义 OOXML - 限制: 没有库能够处理修订更改
结束语
“AI for X” 产品中有趣的工程实现往往并非 AI 本身。
调用 LLM API 很直接。挑战在于其周边的所有工作:
- 保持文档的完整性
- 在长时间运行的操作中管理状态
- 构建验证层,以在用户看到之前捕获模型失效
法律审阅(redlining)迫使我解决了一些未曾预料的问题——段落锚定、OOXML 操作、基于 token 的差异比较。每个解决方案都来源于对业务领域的深入理解,而不是寻找更好的提示。
如果你正在这个领域构建,我很想了解你的做法。
Arun Venkataramanan 是 Ottimate 的高级软件工程师,负责为应付账款自动化构建架构解决方案。其背景涵盖核心银行系统(TCS)、金融科技平台和企业自动化,专注于构建帮助用户在日常工作中自动化重复任务的解决方案和工具。
在 LinkedIn 上联系。