法律人的OpenClaw多Agent 协作系统

0 阅读14分钟

引言:为什么我要折腾这套系统

最近一直都在探索如何将 AI 工具集成进法律场景中,此前用 dify 搭建的知识库和工作流系统,已经基本消灭了“AI 幻觉”,但 dify 是企业级的,对我个人来说太重了,而且工作流缺乏真正的“agent”的自主决策。自从 OpenClaw 出现,原本是把他当“维修工”去运维 dify+n8n 系统,但最近越用越顺手,于是将一部分法律场景工具逐步迁移到 OpenClaw 上,这个迁移的过程本身就很有意思,并不是简单的跨行业跨专业,现在已经自己进化成一套 IT+ 法律 + 金融 + 自媒体的 AI 集成系统。

这套系统中主要由 5 个角色组成,各自分工不同,既有合作也有互相监督机制。

不同 agent 可以通过“圆桌会议”共同优化一个项目,也可以展开“自由辩论”论证一个方案

更狠的是,我还设置了一个“魔鬼代言人”agent,他是负责对圆桌会议形成的方案唱反调,挑毛病的,这就可以有效避免我和 AI 的自嗨假象。

为了避免各 agent 重复劳动、浪费 token,我还建立了缓存共享系统

为了防止各 agent 上下文污染,我又建立了上下文管理系统

详细教程如下,也欢迎您提出建议

2. 我的业务需求:

1.要有一个 AI 律师,帮我处理法律工作(本地知识库检索、类案检索、法律文书生成等)

2.要有一个 IT 工程师,能写代码、能维护系统。将我的所有需求用技术实现

3.要有一个编辑,能将我使用 AI 工具实现法律场景的需求,写出自媒体文章,发布在自媒体平台。

这不就是一个天然的"多 Agent 系统"吗?

2.1 AI 协作架构

我这套系统是借鉴了 TradingAgents 的架构,TradingAgents 本身是擅长做股票分析的,我是参考了他的分层决策思路,但完全没有用他这套系统,我的思路是设计了 5 个 AI 角色:

老 Q (我)
  ↓
傻龙 (主助理) — 主角色
  ↓
┌───────────────┬───────────────┬───────────────┐
↓               ↓               ↓
⚖️ 律师         ✍️ 作家          💻 码农
  ↓               ↓               ↓
🔍 审核员 — 质量把关
  ↓
🎓 魔鬼代言人 — 风险评估

各 agent 的功能说明

傻龙:是我的主 agent,负责与我沟通需求和确认验收

律师:负责提出法律需求、处理法律事务

码农:负责将需求用开发技术实现

作家:负责将整个应用成绩写出自媒体文章

审核员:质量控制模块。无论法律文书还是代码质量他都会审核把关

魔鬼代言人:风险评估专家,专门负责"唱反调"!寻找方案漏洞,提出反面意见,防止其他 agent 出现群体思维和确认偏误。

2.2 文件结构 对每个 agent 的功能定义都要单独建立 agents 文件

文件位于/workspace/agents/ 目录下

/root/.openclaw/workspace/
├── agents/                    # 角色定义
│   ├── lawyer.md
│   ├── writer.md
│   ├── coder.md
│   ├── reviewer.md
│   └── devils_advocate.md
├── checklists/                # 检查清单
│   ├── lawyer_checklist.md
│   ├── writer_checklist.md
│   └── coder_checklist.md
├── src/cache/                 # 缓存模块
│   └── ttl_cache.py
└── cache/                     # 缓存数据
    └── cache.db

3. 核心代码:sessions_spawn 完整调用逻辑

3.1 基础调用

基本调用就是随时可以单独调用任何一个 agent 出来解决属于他专业知识范畴内的问题

from openclaw import sessions_spawn

# 召唤律师分身
response = sessions_spawn(
    task="起草一份民间借贷起诉状",
    role="lawyer",
    timeoutSeconds=300
)

3.2 并行调用(圆桌会议)

圆桌会议是让各个 agent,用自己专业视角去规划我的某个想法或优化某个环节

比如我要在自媒体平台发变这篇文章,就可以召唤“律师、作家、码农”三个 agent 来讨论怎么样打造一篇“爆款”法律与 AI 融合的文章。

# 同时召唤三分身
tasks = [
    {"role": "lawyer", "task": "法律风险分析"},
    {"role": "writer", "task": "传播效果分析"},
    {"role": "coder", "task": "技术可行性分析"}
]

results = []
for task in tasks:
    result = sessions_spawn(
        task=task["task"],
        role=task["role"],
        timeoutSeconds=300
    )
    results.append(result)

# 汇总结果
final_output = summarize(results)

如果有重大决策问题,可以加入“魔鬼代言人”agent,他是负责对圆桌会议形成的方案唱反调,挑毛病的。这就避免了我和 AI 的自嗨假象。

3.3 超时时间配置

这里根据不同任务场景设置了子 agent 的工作时长,避免他滥用资源

def get_timeout(task_type: str) -> int:
    """根据任务类型获取超时时间"""
    timeout_config = {
        "simple_query": 60,      # 简单查询
        "consultation": 180,     # 咨询分析
        "review": 180,           # 审核检查
        "scripting": 300,        # 脚本编写
        "ops": 300,              # 系统运维
        "complex_dev": 600       # 复杂开发
    }
    return timeout_config.get(task_type, 300)

4. 角色定义:5 个角色的详细配置

4.1 律师分身 (agents/lawyer.md)

# ⚖️ Lawyer-Agent 律师分身

## 🎯 身份定位

- **专业领域:** 中国法律文书起草、案卷分析、法律检索
- **人格特质:** 严谨、保守、注重证据链完整性
- **思维模式:** 先找法条 → 再套事实 → 最后下结论

## ⚠️ 执业边界

### 可以做
- ✅ 法律文书起草(起诉状、答辩状、代理词等)
- ✅ 案卷分析与证据梳理
- ✅ 法律检索与法条引用

### 禁止做
- ❌ 不给出确定性胜诉承诺
- ❌ 不编造法条或案例

## 🧠 工作流程

1. 接收任务
2. 检索相关法条(必须精确到条款序号)
3. 分析案件事实(区分"事实"与"意见")
4. 起草文书/出具分析
5. 自我审查(检查清单)
6. 交付傻龙汇总

## 📋 输出标准

### 法条引用
- 必须完整:《法律名称》第 X 条第 X 款
- 必须核实有效性

### 事实描述
- 必须标注证据来源
- 必须区分"已证实事实"与"待证事实"

4.2 作家分身 (agents/writer.md)

# ✍️ Writer-Agent 作家分身

## 🎯 身份定位

- **专业领域:** 自媒体创作、洗稿、观点提炼
- **人格特质:** 敏锐、有温度、跨界类比
- **思维模式:** 找核心冲突 → 提炼金句 → 构建叙事弧线

## 📋 输出标准

### 标题
- 必须有吸引力但不夸大
- 必须提供 3-5 个备选

### 正文
- 开头:3 秒内抓住注意力
- 每段:至少 1 个可传播金句
- 结尾:行动号召或深度思考

4.3 码农分身 (agents/coder.md)

# 💻 Coder-Agent 码农分身

## 🎯 身份定位

- **专业领域:** Python 脚本、运维、故障排查
- **人格特质:** 严谨、注重边界、先解释后指令

## ⚠️ 运维边界

### 可以做
- ✅ Python 自动化脚本开发
- ✅ 系统配置检查与优化建议

### 禁止做
- ❌ 不执行危险命令(rm -rf /等)
- ❌ 不修改系统核心配置(/etc/ 下文件)

## 📋 输出标准

### 命令解释
- 每条命令前必须解释作用
- 危险操作前必须**加粗提示**风险

4.4 审核员 (agents/reviewer.md)

# 🔍 Reviewer 审核员

## 🎯 身份定位

- **职责:** 查找低级错误和逻辑漏洞
- **人格:** 挑剔、细节控
- **口头禅:** "让我再检查一遍"

## 🧠 工作流程

接收分身输出 → 逐项检查清单 → 标注问题 → 决策

## 📋 决策权限

- ✅ 放行:无致命问题
- 🔄 要求修改:有严重问题(限 1 次)
- 🚨 上报傻龙:有致命问题未修复

4.5 魔鬼代言人 (agents/devils_advocate.md)

# 🎓 Devil's Advocate 魔鬼代言人

## 🎯 身份定位

- **职责:** 寻找方案漏洞,提出反面意见
- **人格:** 怀疑主义、批判性思维
- **触发条件:** 重大决策、高风险操作

## 🧠 思维工具

1. **假设挑战**:这个方案的核心假设是什么?
2. **最坏情况推演**:最坏情况是什么?
3. **逆向思考**:如果反过来做会怎样?
4. **利益相关者分析**:谁会反对?

## 📋 输出形式

### 🔴 致命风险(必须解决)
### 🟠 严重风险(强烈建议解决)
### 💡 替代方案
### 🌳 决策树

5. 检查清单:审核员如何逐项检查

5.1 律师检查清单 (checklists/lawyer_checklist.md)

# ⚖️ 律师输出检查清单

## 🔴 致命问题(必须修复)

### 法条引用
- [ ] 法条名称是否完整准确?
- [ ] 条款序号是否正确?
- [ ] 法条是否有效?

### 事实描述
- [ ] 是否编造了事实或证据?
- [ ] 是否区分了"已证实事实"与"待证事实"?

## 🟠 严重问题(建议修复)

### 逻辑自洽
- [ ] 结论是否有法条支撑?
- [ ] 推理过程是否跳跃?

### 风险提示
- [ ] 是否提示了诉讼风险?
- [ ] 是否给出了备选方案?

## 🟡 轻微问题(标注即可)

### 表达
- [ ] 是否有错别字?
- [ ] 是否有语病?

5.2 审核员代码实现

class Reviewer:
    """审核员实现"""
    
    def __init__(self, checklist_path: str):
        self.checklist = self.load_checklist(checklist_path)
    
    def review(self, content: str, role: str) -> dict:
        """审核输出内容"""
        issues = {
            "fatal": [],
            "serious": [],
            "minor": []
        }
        
        # 根据角色选择检查清单
        checklist = self.checklist[role]
        
        # 逐项检查
        for item in checklist:
            if not self.check(content, item):
                issues[item["level"]].append(item["description"])
        
        # 决策
        if issues["fatal"]:
            return {"decision": "escalate", "issues": issues}
        elif issues["serious"]:
            return {"decision": "revise", "issues": issues}
        else:
            return {"decision": "approved", "issues": issues}
    
    def check(self, content: str, item: dict) -> bool:
        """执行单项检查"""
        # 实现具体检查逻辑
        pass

6. 缓存实现:从 0 到 1 构建缓存系统

6.1 为什么需要缓存?

早期我遇到过一个问题:同一个任务,律师分析了一遍,作家又分析了一遍,码农再分析一遍——很多基础信息是重复的。

比如"写一篇 AI 法律风险文章",三分身都要先了解:

  • AI 是什么
  • 有哪些法律风险
  • 目标读者是谁

每次都重新分析,浪费时间浪费 token。

6.2 数据库设计

CREATE TABLE cache (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    key TEXT UNIQUE NOT NULL,
    value BLOB NOT NULL,          -- 压缩后的数据
    created_at REAL NOT NULL,     -- 创建时间戳
    ttl INTEGER NOT NULL,         -- 生存时间(秒)
    category TEXT DEFAULT 'default',
    access_count INTEGER DEFAULT 0,
    last_access_at REAL
);

CREATE INDEX idx_key ON cache(key);
CREATE INDEX idx_category ON cache(category);
CREATE INDEX idx_expires ON cache(created_at + ttl);

6.3 TTL 缓存完整代码

import sqlite3
import hashlib
import gzip
import json
import time
import threading
from typing import Any, Optional
from contextlib import contextmanager

class TtlCache:
    """带锁、带压缩、带自动过期的 TTL 缓存"""
    
    def __init__(self, db_path: str, max_size: int = 10000):
        self.db_path = db_path
        self.max_size = max_size
        self.locks = {}
        self.locks_lock = threading.Lock()
        self._init_db()
    
    def _init_db(self):
        """初始化数据库"""
        with self._get_cursor() as cursor:
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS cache (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    key TEXT UNIQUE NOT NULL,
                    value BLOB NOT NULL,
                    created_at REAL NOT NULL,
                    ttl INTEGER NOT NULL,
                    category TEXT DEFAULT 'default',
                    access_count INTEGER DEFAULT 0,
                    last_access_at REAL
                )
            ''')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_key ON cache(key)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON cache(category)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_expires ON cache(created_at + ttl)')
    
    def _get_lock(self, key: str) -> threading.Lock:
        """获取 key 的锁(防止缓存击穿)"""
        with self.locks_lock:
            if key not in self.locks:
                self.locks[key] = threading.Lock()
            return self.locks[key]
    
    def get(self, key: str) -> Optional[Any]:
        """获取缓存"""
        lock = self._get_lock(key)
        with lock:
            now = time.time()
            with self._get_cursor() as cursor:
                cursor.execute('''
                    SELECT value FROM cache
                    WHERE key = ? AND (created_at + ttl) > ?
                ''', (self._hash(key), now))
                row = cursor.fetchone()
                if row:
                    return self._decompress(row['value'])
                return None
    
    def set(self, key: str, value: Any, ttl: int = 3600):
        """设置缓存"""
        now = time.time()
        compressed = self._compress(value)
        with self._get_cursor() as cursor:
            cursor.execute('''
                INSERT OR REPLACE INTO cache
                (key, value, created_at, ttl)
                VALUES (?, ?, ?, ?)
            ''', (self._hash(key), compressed, now, ttl))
            
            # 清理过期缓存
            cursor.execute(
                'DELETE FROM cache WHERE (created_at + ttl) < ?',
                (now,)
            )
    
    def get_or_set(self, key: str, fetch_func: callable, ttl: int = 3600):
        """获取或设置缓存(原子操作)"""
        cached = self.get(key)
        if cached is not None:
            return cached
        
        value = fetch_func()
        self.set(key, value, ttl)
        return value
    
    def _compress(self, data: Any) -> bytes:
        """压缩数据"""
        json_str = json.dumps(data, ensure_ascii=False)
        return gzip.compress(json_str.encode('utf-8'))
    
    def _decompress(self, compressed: bytes) -> Any:
        """解压数据"""
        json_str = gzip.decompress(compressed).decode('utf-8')
        return json.loads(json_str)
    
    def _hash(self, key: str) -> str:
        """计算 key 的哈希"""
        return hashlib.md5(key.encode('utf-8')).hexdigest()
    
    @contextmanager
    def _get_cursor(self):
        """获取游标上下文管理器"""
        conn = sqlite3.connect(self.db_path, check_same_thread=False, timeout=30)
        conn.row_factory = sqlite3.Row
        cursor = conn.cursor()
        try:
            yield cursor
            conn.commit()
        except Exception as e:
            conn.rollback()
            raise e
        finally:
            cursor.close()

6.4 使用示例

from src.cache.ttl_cache import TtlCache

# 初始化缓存
cache = TtlCache("/root/.openclaw/workspace/cache/cache.db")

# 基本使用
cache.set("user_123", {"name": "老 Q"}, ttl=3600)
user_data = cache.get("user_123")

# 使用 get_or_set(推荐)
def fetch_github_data():
    return {"stars": 100}

data = cache.get_or_set("github_stars", fetch_github_data, ttl=7200)

7. 性能数据:优化前后的真实对比

7.1 优化前的问题

问题 1:重复分析浪费资源

老 Q 指令 → 律师分析(完整背景)→ 作家分析(完整背景)→ 码农分析(完整背景)
耗时:45 分钟

问题 2:无缓存机制

每次调用 API 都要重新请求
每次数据库查询都要重新执行

7.2 优化后的实现

优化 1:共享上下文

class RoundTable:
    """圆桌会议协调器"""
    
    def __init__(self):
        self.shared_context = {}
    
    def analyze(self, task: str):
        # 傻龙解析背景(一次)
        self.shared_context = self.parse_background(task)
        
        # 三分身并行分析(只分析专业部分)
        results = parallel_call([
            {"role": "lawyer", "context": self.shared_context},
            {"role": "writer", "context": self.shared_context},
            {"role": "coder", "context": self.shared_context}
        ])
        
        return summarize(results)

优化 2:结果缓存

# 缓存常见问题模板
cache.set("complaint_template", template, ttl=86400)

# 缓存脚本
cache.set("backup_script", script, ttl=604800)

# 缓存标题库
cache.set("title_formulas", formulas, ttl=2592000)

7.3 性能数据对比

指标优化前优化后提升
跨领域分析耗时45 分钟25 分钟44%
API 响应时间200-500ms<1ms(缓存命中)99%+
重复任务耗时20 分钟3 分钟85%
低级错误率15%1.2%92%

8. 部署指南:一步步教

8.1 环境准备

# 创建目录
mkdir -p /root/.openclaw/workspace/{agents,checklists,src/cache,cache}

# 确认 Python 版本(需要 3.12+)
python3 --version

8.2 创建角色定义文件

# 律师分身
cat > /root/.openclaw/workspace/agents/lawyer.md << 'EOF'
# ⚖️ Lawyer-Agent 律师分身

## 🎯 身份定位

- **专业领域:** 中国法律文书起草、案卷分析、法律检索
- **人格特质:** 严谨、保守、注重证据链完整性

## ⚠️ 执业边界

### 可以做
- ✅ 法律文书起草(起诉状、答辩状、代理词等)
- ✅ 案卷分析与证据梳理

### 禁止做
- ❌ 不给出确定性胜诉承诺
- ❌ 不编造法条或案例
EOF

# 作家分身
cat > /root/.openclaw/workspace/agents/writer.md << 'EOF'
# ✍️ Writer-Agent 作家分身
# (复制完整定义)
EOF

# 码农分身
cat > /root/.openclaw/workspace/agents/coder.md << 'EOF'
# 💻 Coder-Agent 码农分身
# (复制完整定义)
EOF

# 审核员
cat > /root/.openclaw/workspace/agents/reviewer.md << 'EOF'
# 🔍 Reviewer 审核员
# (复制完整定义)
EOF

# 魔鬼代言人
cat > /root/.openclaw/workspace/agents/devils_advocate.md << 'EOF'
# 🎓 Devil's Advocate 魔鬼代言人
# (复制完整定义)
EOF

8.3 创建检查清单

# 律师检查清单
cat > /root/.openclaw/workspace/checklists/lawyer_checklist.md << 'EOF'
# ⚖️ 律师输出检查清单

## 🔴 致命问题(必须修复)

### 法条引用
- [ ] 法条名称是否完整准确?
- [ ] 条款序号是否正确?
- [ ] 法条是否有效?
EOF

# 作家检查清单
cat > /root/.openclaw/workspace/checklists/writer_checklist.md << 'EOF'
# ✍️ 作家输出检查清单
# (复制完整清单)
EOF

# 码农检查清单
cat > /root/.openclaw/workspace/checklists/coder_checklist.md << 'EOF'
# 💻 码农输出检查清单
# (复制完整清单)
EOF

8.4 部署缓存模块

# 复制缓存代码
cp /root/.openclaw/workspace/src/cache/ttl_cache.py \
   /your/project/src/cache/

# 初始化缓存
python3 -c "from src.cache.ttl_cache import init_cache_db; init_cache_db()"

# 测试缓存
python3 -c "
from src.cache.ttl_cache import TtlCache
cache = TtlCache('cache/cache.db')
cache.set('test', {'hello': 'world'})
print(cache.get('test'))
"

8.5 验证部署

# 运行测试脚本
python3 /root/.openclaw/workspace/examples/cache_example.py

# 预期输出:
# ✅ 缓存数据库初始化完成
# 测试 1 - 设置/获取:{'hello': 'world'}
# 测试 2 - 第一次获取:{'data': 'fresh'}
# 测试 2 - 第二次获取(缓存命中):{'data': 'fresh'}
# ✅ 测试完成

9. 踩坑记录:我犯过的错误和解决方案

踩坑 1:法条引用错误

问题: 在一份法律意见书中引用了已经废止的法条。

原因: 没有核实法条有效性。

解决方案: 律师检查清单中加入"法条是否有效"检查项。

踩坑 2:系统迁移数据丢失

问题: 迁移系统时数据丢失 20%。

原因: 没有做完整备份。

解决方案: 魔鬼代言人必须评估"最坏情况",并提供 B 计划。

踩坑 3:缓存击穿

问题: 高并发时缓存失效,大量请求直接打到数据库。

原因: 没有加锁。

解决方案: 每个 key 独立锁,防止并发请求击穿缓存。

踩坑 4:审核员流于形式

问题: 审核员快速扫一眼就放行。

原因: 没有强制检查清单。

解决方案: 审核员必须逐项检查清单,不能跳过。

踩坑 5:分身上下文污染

问题: 上一个任务的上下文影响下一个任务。

原因: 任务结束后没有释放资源。

解决方案: 任务结束后立即执行 subagents(action="kill")

结语:给想动手的朋友的建议

作为一个法律人,我不需要成为技术专家,但我需要知道如何用技术解决法律问题。

这套系统的核心价值,不是技术有多先进,而是​把专业知识结构化​。

律师知道怎么引用法条,作家知道怎么制造金句,码农知道怎么标注风险——这些专业知识被固化在角色定义和检查清单里,AI 只需要按图索骥。