当 LLM 开始骗自己:一个用正则表达式守护文章可信度的多 Agent 框架

作者:

🇬🇧 English Version

多 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))

这个函数做了两件关键事情:

  1. 提取所有反引号包裹的代码标识符:包括文件路径、函数名、类名
  2. 从代码块中提取 defclass 声明:确保代码示例中的关键标识符也被验证

然后,它对每个引用进行验证:

    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",
        # ... 更多关键字和内置函数
    }

如果不排除这些关键字,文章中出现 defclass 这样的词会被误报为虚构代码引用。

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
  • 技术文档:确保文档中的示例和引用与实际代码库一致
  • 研究报告:防止模型编造不存在的实验结果或参考文献

启示

  1. 不要过度依赖 LLM 的自我评估:让 LLM 检查 LLM 的输出是不可靠的
  2. 程序化验证是关键:用确定性的检查替代概率性的判断
  3. 独立于编排的修复机制:绕过复杂的 Agent 流程,直接用 LLM API 进行精确修复
  4. 安全沙箱必不可少:即使是对"助手"类型的 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

配置

  1. 复制 .env.example.env,填入你的 OpenAI API Key
  2. 设置 PSE_ROOT 环境变量指向你的项目根目录
  3. tasks/project-articles/projects.json 中添加项目配置

运行

cd tasks/project-articles
python run.py <项目名>

例如:

python run.py my-project

这将自动生成该项目的中文技术文章,并附带英文翻译。

这篇文章是通过 crewai-pse 框架本身生成的——Planner 规划了结构,Specialist 撰写了内容,程序化验证确保了所有代码引用和类名的准确性。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

首页 简历 关于 隐私政策

© 2026 Erishen
沪ICP备2024079226号-1   沪公网安备31010502007082号