构建可靠 LLM 应用的 6 个防御策略:pdf2anki 开发实战

作者
  • avatar
    姓名
    Nino
    职业
    Senior Tech Editor

在集成大语言模型(LLM)的世界里,每位开发者最终都会面临一个残酷的事实:LLM 是概率性的,而非确定性的。当我开始构建 pdf2anki(一个旨在将复杂的学术 PDF 转换为高质量 Anki 记忆卡的命令行工具)时,我很快意识到,仅仅调用 API 是远远不够的。为了创造真正实用的工具,我必须围绕 Claude API 构建一套坚固的“防御性代码”。

本教程将详细介绍我实施的六个核心防御策略。无论你是使用 n1n.ai 来访问 Claude 3.5 Sonnet,还是使用 OpenAI 的最新模型,这些模式都将帮助你构建更具韧性的 AI 驱动工具。

1. “部分成功”解析器:拒绝全盘否定

当你要求 LLM 返回 JSON 格式时,它通常会配合。但在大规模处理时,总会出现意外。Claude 可能会包含 Markdown 代码块(json ... )、添加解释性文字,或者幻化出一个破坏标准解析器的多余逗号。

最初,我的工具采用的是“要么全有,要么全无”的方法。如果一组 10 张卡片中有一个格式错误,整个请求就会失败,我必须重新重试。这不仅浪费了 Token,也浪费了金钱。

防御方案:细粒度验证

我实现了一个循环,使用 Pydantic 验证单个对象,而不是解析整个响应。如果某张卡片失败,我们会记录并跳过它,保留其他成功的卡片。

from pydantic import BaseModel, ValidationError
import json
import re

class AnkiCard(BaseModel):
    front: str
    back: str
    tags: list[str]

def robust_parse(raw_output: str):
    # 使用正则剥离 Markdown 围栏
    cleaned_json = re.sub(r'^```json\s*|\s*```$', '', raw_output.strip(), flags=re.MULTILINE)
    try:
        data = json.loads(cleaned_json)
    except json.JSONDecodeError:
        return []

    valid_cards = []
    for i, item in enumerate(data):
        try:
            # 验证单个对象
            card = AnkiCard.model_validate(item)
            valid_cards.append(card)
        except (ValidationError, TypeError) as e:
            print(f"跳过索引为 {i} 的无效卡片: {e}")
    return valid_cards

专家提示: 永远假设 LLM 在某些时候会出错。通过使用 n1n.ai,你可以轻松地在 Claude 3.5 Sonnet 和 Haiku 之间切换,测试哪种模型在负载下更能严格遵守你的 JSON Schema。

2. 启发式质量过滤:成本与质量的博弈

LLM 有时会生成“偷懒”的内容。对于记忆卡片来说,这意味着卡片可能太长、太模糊,或者在一个卡片中塞入了多个概念。让 LLM 对每张卡片进行“自我批判”是一个选择,但这会使你的 API 成本翻倍。

防御方案:基于代码的评分系统

我构建了一个多层过滤系统。第一层是纯 Python 代码,不涉及 LLM。它根据启发式算法对卡片进行评分:

维度权重逻辑
原子性 (Atomicity)25%如果“背面”超过 3 句或使用“此外”等连词,则扣分。
长度 (Length)25%理想长度为 10–200 个字符。
格式 (Formatting)25%检查是否包含问号或特定关键词。
唯一性 (Uniqueness)25%使用 Jaccard 相似度检测批次中的重复卡片。

只有得分低于 0.90 阈值的卡片才会发回给 LLM 进行“批判与重写”。这使我的 API 使用量减少了近 60%。

3. 财务护栏:让成本透明化

API 成本可能会失控,尤其是在处理 500 页的 PDF 时。如果你的工具在执行前不向用户显示价格,那就是一种责任缺失。

防御方案:预执行估算与硬性上限

我实现了一个 CostTracker,在进行任何 API 调用之前,根据字符数计算估计成本。

@dataclass(frozen=True)
class CostTracker:
    budget_limit: float = 1.00
    current_spend: float = 0.0

    def check_budget(self, additional_cost: float):
        if self.current_spend + additional_cost > self.budget_limit:
            raise Exception("预算超支!处理停止。")

通过利用 n1n.ai 的高速基础设施,开发者可以获得稳定的延迟,确保预算逻辑不会拖慢用户体验。

4. 语义化文档切分:保留上下文

将整个 PDF 喂给 LLM 是导致上下文丢失的诱因。然而,每隔 2000 个字符机械地切分 PDF 同样糟糕,因为它可能会切断一个句子或一个章节。

防御方案:Markdown 标题面包屑

我转向了基于标题的切分策略。工具会跟踪当前的 # H1## H2### H3。发送给 LLM 的每个文本块都会加上“面包屑”前缀:上下文:第二章 > 2.1 节 > 主题 A

这确保了即使文本块很小,LLM 也能准确知道它在文档层次结构中的位置,从而生成更准确的卡片。

5. 务实的视觉集成:昂贵的代价

视觉模型(如 Claude 3.5 Sonnet)功能强大但价格昂贵。将 PDF 的每一页都转换为图像,其成本比纯文本提取高出约 7 倍。

防御方案:20% 覆盖率规则

我的工具首先使用 pymupdf 分析页面布局:

  1. 如果页面的图像区域 < 20%,我们只提取文本。
  2. 如果超过 20%,我们以 150 DPI 渲染页面(这是清晰度与 Token 成本的平衡点)并发送至 Vision API。
  3. 我们限制每页最多处理 5 张图像,以防止 Token 膨胀。

6. 自动化评估 (Evals):告别凭感觉调优

如果你修改了提示词(Prompt),你如何知道它真的变好了?“感觉”不是一个衡量指标。

防御方案:基于关键词的评估框架

我创建了一个基于 YAML 的“黄金标准”案例数据集。当我更新提示词时,工具会运行自动化评估,通过召回率(Recall)和准确率(Precision)指标将 LLM 输出与预期关键词进行比较。

- id: 'concept-01'
  text: '光合作用是植物利用阳光合成养分的过程...'
  expected_keywords: ['阳光', '养分', '叶绿素']

即使是简单的关键词匹配,也足以检测提示词的更改是否导致了质量倒退。

总结

构建 pdf2anki 的过程让我明白,LLM 只是引擎;周围的代码是底盘、刹车和仪表盘。通过实施验证、启发式过滤和语义切分,你可以将一个脆弱的脚本转变为一个健壮的专业工具。

准备好构建你自己的可靠 LLM 应用了吗?立即在 n1n.ai 获取免费 API 密钥。