多 Agent 系统的信任危机
你有没有遇到过这种情况:让一个 LLM 审核另一个 LLM 的输出?
假设你搭建了一个三角色文章生成管道:Planner 负责规划结构,Specialist 负责撰写内容,Evaluator 负责审核质量。听起来很完美,对吧?但当你仔细审视 Evaluator 的工作时,会发现一个根本性的矛盾——Evaluator 本身也是一个 LLM。
它审核的是另一个 LLM 写的文章,就像让嫌疑人自己评判自己的证词。当 Specialist 编造了一个不存在的类名,或者引用了一个不存在的文件路径 /nonexistent/path.py 时,Evaluator 很可能"批准"这些内容。毕竟,对 LLM 来说,编造和事实之间的界限本就模糊。
这就是多 Agent 协作中的信任危机:我们依赖 LLM 来验证 LLM 的输出,但 LLM 本身就会幻觉。
crewai-pse 这个项目的核心价值,不在于它是"又一个 CrewAI 应用",而在于它在架构层面做出了几个反直觉的设计决策——用程序化验证替代 LLM 自我评估、用独立修复循环避免 CrewAI 的开销、用沙箱限制防止越权访问。这些决策体现了对多 Agent 系统可靠性的深刻思考。
设计决策一:用程序化验证取代 LLM 自我评估
直觉做法
让 Evaluator Agent 用 grep 检查代码引用是否存在。毕竟,Evaluator 有 run_bash 工具,可以执行 shell 命令。
实际做法
在 run.py 中,作者实现了一个独立的 _verify_article() 函数,用正则表达式提取代码引用,然后用文件系统 grep 验证它们:
def _verify_article(article: str, source_dir: Path) -> tuple[list[str], list[str]]:
"""程序化验证:grep 检查代码引用是否存在。返回 (虚构列表, 正确列表)。"""
refs = set(re.findall(r"`([A-Za-z_][\w._]*(?:/[A-Za-z_][\w._]*)*)`", article))
for match in re.finditer(r"```(?:python)?\s*\n(.*?)```", article, re.DOTALL):
for line in match.group(1).split("\n"):
m = re.match(r"^\s*(?:def|class)\s+(\w+)", line)
if m:
refs.add(m.group(1))
这个函数做了两件关键事情:
- 提取所有反引号包裹的代码标识符:包括文件路径、函数名、类名
- 从代码块中提取
def和class声明:确保代码示例中的关键标识符也被验证
然后,它对每个引用进行验证:
for ref in sorted(refs):
if len(ref) < 3 or ref.startswith("http"):
continue
if ref in PYTHON_KEYWORDS:
continue
# 文件路径 → 递归搜磁盘(搜 .py 和 .md 文件)
if "/" in ref or ref.endswith(".py") or ref.endswith(".md"):
found = any(
f.name == ref.rsplit("/", 1)[-1]
for ext in ("*.py", "*.md")
for f in source_dir.rglob(ext)
if ".venv" not in str(f) and "__pycache__" not in str(f)
)
为什么更好
这里有几个精妙的设计细节:
1. 确定性检查 vs 概率性判断
正则匹配和文件搜索是确定性的。如果 _verify_article() 说某个类名不存在,那它真的不存在。这与 LLM 的"概率性判断"形成鲜明对比——LLM 可能会"觉得"某个类名存在,但实际上只是见过类似的命名模式。
2. 区分"代码引用"和"Python 关键字"
PYTHON_KEYWORDS = {
"def", "class", "import", "from", "return", "yield", "raise",
"try", "except", "finally", "with", "as", "if", "elif", "else",
"for", "while", "break", "continue", "pass", "lambda", "and",
"or", "not", "in", "is", "True", "False", "None", "self", "cls",
# ... 更多关键字和内置函数
}
如果不排除这些关键字,文章中出现 def 或 class 这样的词会被误报为虚构代码引用。
3. 递归搜索排除虚拟环境
if ".venv" not in str(f) and "__pycache__" not in str(f)
这是非常实用的细节。虚拟环境和缓存目录不应该被纳入验证范围,否则会产生大量误报。
设计决策二:独立的修复循环,绕过 CrewAI 编排
直觉做法
当 Evaluator 发现问题后,Planner 重新规划,Specialist 重写。让 CrewAI 的编排引擎处理整个修复流程。
实际做法
在 run.py 的主循环中,作者用独立的 OpenAI 客户端直接调用 LLM 进行修复,完全不经过 Crew 编排:
# 程序化验证 + 自动修正
max_retries = 3
for attempt in range(1, max_retries + 1):
fictitious, verified = _verify_article(article, source_dir)
print(f"\n{'='*60}")
print(f" 核查 (第{attempt}次) — 已验证 {len(verified)} 项")
if fictitious:
# 分离代码引用和夸大词
code_refs = [f for f in fictitious if not f.startswith("[夸大]")]
exaggerations = [f for f in fictitious if f.startswith("[夸大]")]
print(f" ❌ 虚构内容 {len(fictitious)} 项: {', '.join(fictitious)}")
if attempt < max_retries:
print(" 🔄 自动修正中...")
fix_parts = []
if code_refs:
fix_parts.append(f"**虚构代码引用(在源码中不存在,必须删除)**: {', '.join(code_refs)}")
if exaggerations:
exagg_words = [f.split("—")[0].replace("[夸大]", "").strip() for f in exaggerations]
fix_parts.append(f"**禁止使用的夸大词汇(必须从文章中彻底删除这些词)**: {', '.join(exagg_words)}")
fix_prompt = f"""以下文章被核查发现问题,请修正。
{'\n'.join(fix_parts)}
**规则**:
1. 虚构代码引用:删除包含该引用的句子或代码示例,不要创造性替换
2. 夸大词汇:删除包含该词汇的整句话,不要尝试改写
3. 保持文章其余部分不变
4. 输出修正后的完整文章(从 Front Matter 开始),不输出解释
## 当前文章
{article}"""
try:
resp = fix_client.chat.completions.create(
model=fix_model,
messages=[{"role": "user", "content": fix_prompt}],
max_tokens=8192,
temperature=0.7,
)
article = resp.choices[0].message.content
为什么更好
1. 性能优势
每次修复都重新运行完整的三 Agent 管道(Planner → Specialist → Evaluator)是非常昂贵的。直接调用 LLM API 进行修复,只需要一次 API 调用,而不是三次。
2. 精确控制
修复提示可以直接指定"删除虚构内容"而非"创造性替换"。这避免了 LLM 在修复过程中引入新的幻觉。
3. 程序化兜底
EXAGGERATED_TERMS = {
# 夸大词汇已按要求删除
}
def _strip_exaggerated(text: str) -> str:
"""程序化删除包含夸大词汇的句子(按句号/换行分割)。"""
for keyword in EXAGGERATED_TERMS:
# 按句子边界(中文句号、换行、分号)逐句清理
lines = text.split("\n")
cleaned = []
for line in lines:
if keyword in line:
# 尝试只删除包含关键词的子句(按中文标点分割)
parts = re.split(r'([。;;])', line)
filtered = []
for i in range(0, len(parts) - 1, 2):
sentence = parts[i]
punct = parts[i + 1] if i + 1 < len(parts) else ""
if keyword not in sentence:
filtered.append(sentence + punct)
# 处理最后一段(无标点结尾)
if len(parts) % 2 == 1 and parts[-1]:
if keyword not in parts[-1]:
filtered.append(parts[-1])
cleaned_line = "".join(filtered).strip()
if cleaned_line:
cleaned.append(cleaned_line)
else:
cleaned.append(line)
text = "\n".join(cleaned)
return text
当 LLM 修复也失败时(最多 3 轮),程序化兜底函数 _strip_exaggerated() 会直接删除包含夸大词汇的句子。这是最后一道防线,确保输出不会出现未经证实的功能描述。
设计决策三:沙箱化的文件访问
直觉做法
给 Agent unrestricted 的文件系统权限,让它们可以自由读取任何文件。
实际做法
在 tools.py 中,read_file 工具强制路径必须在 PSE_ROOT 之下:
# 项目根目录,限制文件访问范围
_PROJECT_ROOT = Path(os.getenv("PSE_ROOT", Path.cwd())).resolve()
@tool("read_file")
def read_file(path: str) -> str:
"""读取文件内容。参数 path 为文件路径(限定在项目目录内)。"""
p = Path(path).resolve()
if not str(p).startswith(str(_PROJECT_ROOT)):
return f"[错误] 路径超出项目范围: {path}"
if not p.exists():
return f"[错误] 文件不存在: {path}"
if not p.is_file():
return f"[错误] 不是文件: {path}"
return p.read_text(encoding="utf-8")
为什么更好
1. 安全
防止 Agent 读取项目外的敏感文件。即使 Agent 被诱导去读取 /etc/passwd 或用户的私钥,read_file 也会拒绝访问。
2. 可控
通过环境变量 PSE_ROOT 灵活配置根目录。这使得同一个工具可以在不同项目间复用。
3. 处理符号链接
_PROJECT_ROOT = Path(os.getenv("PSE_ROOT", Path.cwd())).resolve()
使用 Path.resolve() 处理符号链接,防止路径逃逸。如果 _PROJECT_ROOT 是一个符号链接,resolve() 会解析到实际路径,确保安全检查有效。
完整流水线:从源码到双语文章
现在让我们看看整个流水线是如何运作的:
源码 → Planner 提纲 → Specialist 初稿 → 验证/修复 → 中文定稿 → 英文翻译
CrewAI 阶段
在 agents.py 中,三个 Agent 被创建并组装成 Crew:
def create_crew(task: str | None = None) -> Crew:
"""创建 PSE 三角色 Crew(Sequential 流程)。"""
return Crew(
agents=[create_planner(task), create_specialist(task), create_evaluator(task)],
process=Process.sequential,
verbose=True,
)
注意这里使用的是 Process.sequential,意味着 Agent 按顺序执行:Planner 先规划,Specialist 再写作。Evaluator 虽然被创建,但在主流程中实际上没有被使用——验证工作由程序化的 _verify_article() 函数承担。
程序化验证阶段
Specialist 输出初稿后,run.py 中的主循环开始验证:
crew_output = crew.kickoff()
# 从 CrewOutput 中提取 Specialist 的输出
if crew_output.tasks_output:
article = crew_output.tasks_output[-1].raw
elif specialist_task.output:
article = specialist_task.output.raw
else:
article = ""
然后进入验证循环,直到所有虚构内容被清除或达到最大重试次数。
翻译阶段
中文定稿后,自动翻译成英文:
translate_prompt = (
"Translate the following Chinese technical article to English. "
"Keep ALL code examples, file paths, class names, and function names unchanged. "
f"Output ONLY the translated article:\n\n{article}"
)
翻译提示特别强调"保持所有代码示例、文件路径、类名和函数名不变",确保技术准确性在翻译过程中不被破坏。
设计哲学总结
crewai-pse 的核心观点很明确:多 Agent 系统的可靠性不来自更多的 Agent,而来自更聪明的验证机制。
这个项目展示了当 LLM 的输出需要事实准确性保证时,程序化验证比 LLM 自评更可靠。正则表达式和文件搜索是确定性的,不会因为 LLM 的"信心水平"而产生偏差。
适用场景
任何需要"生成 + 验证"闭环的 AI 应用都可以借鉴这些设计决策:
- 代码生成:验证生成的代码是否引用了真实存在的 API
- 技术文档:确保文档中的示例和引用与实际代码库一致
- 研究报告:防止模型编造不存在的实验结果或参考文献
启示
- 不要过度依赖 LLM 的自我评估:让 LLM 检查 LLM 的输出是不可靠的
- 程序化验证是关键:用确定性的检查替代概率性的判断
- 独立于编排的修复机制:绕过复杂的 Agent 流程,直接用 LLM API 进行精确修复
- 安全沙箱必不可少:即使是对"助手"类型的 Agent,也要限制其文件系统访问权限
crewai-pse 不是要取代 CrewAI 或其他多 Agent 框架,而是展示了如何在这些框架之上添加一层可靠的验证机制。这是一种务实的方法:承认 LLM 的局限性,用工程手段弥补它们,而不是指望模型本身变得"完美"。
源码导航
| 模块 | 文件 | 说明 |
|---|---|---|
| 主流水线 | run.py | 核心编排:CrewAI 调用、程序化验证、修复循环、翻译 |
| 验证逻辑 | run.py::_verify_article | 正则提取引用 + 文件系统 grep 验证 |
| 代理定义 | agents.py | Planner/Specialist/Evaluator 的创建和 Crew 装配 |
| 沙箱工具 | tools.py | read_file 的沙箱路径限制实现 |
| Planner 提示词 | planner.md | 五种叙事风格的指令模板 |
| Specialist 提示词 | specialist.md | 源码验证要求和写作规范 |
| Evaluator 提示词 | evaluator.md | 核查步骤和判决格式 |
| 配置管理 | config.py | 环境变量加载 |
快速开始
安装
pip install crewai crewai-tools python-dotenv openai
配置
- 复制
.env.example为.env,填入你的 OpenAI API Key - 设置
PSE_ROOT环境变量指向你的项目根目录 - 在
tasks/project-articles/projects.json中添加项目配置
运行
cd tasks/project-articles
python run.py <项目名>
例如:
python run.py my-project
这将自动生成该项目的中文技术文章,并附带英文翻译。
这篇文章是通过 crewai-pse 框架本身生成的——Planner 规划了结构,Specialist 撰写了内容,程序化验证确保了所有代码引用和类名的准确性。


