第三章:Prompt 工程化:使用 LangChain 与 Promptfoo 构建、评估与优化

章节引导:在上一章,我们成功地为 LLM 应用装配了稳定可靠的“输入输出管道”——通过 litellm 统一了 API 调用,并利用 instructor 获取了结构化的输出。现在,我们的焦点转向了这条管道中流动的核心内容:我们发送给 LLM 的指令,即 Prompt。仅仅拥有通畅的管道是不够的,管道中传输的内容质量直接决定了最终结果的好坏。本章将带你实现一次关键的角色转变——从临时的 Prompt “手艺人”升级为系统的 Prompt “工程师”。我们将学习如何使用 LangChain 构建可维护、可组合的 Prompt 结构,并利用 Promptfoo 对其进行量化评估与迭代优化,告别“凭感觉”调优的时代。

3.1 从经验到工程:为何需要 Prompt 工程框架?

在 LLM 应用开发的初期,我们往往凭直觉编写 Prompt。对于简单任务,这或许可行。但随着应用逻辑变复杂、需求迭代加快,这种“手工作坊”式的 Prompt 开发很快会暴露出诸多问题,演变成一场难以管理的混乱:

  • 一致性难保: 同样的任务,不同时间、不同开发者写的 Prompt 可能千差万别,导致模型输出质量不稳定。
  • 难以复用: 优秀的 Prompt 片段或结构无法方便地在项目不同部分或不同项目间共享和复用,造成重复劳动。
  • 版本管理混乱: Prompt 的微小改动可能导致模型行为巨大变化。如何追踪哪个版本的 Prompt 对应哪个模型版本和应用版本?没有系统化管理,这几乎是不可能完成的任务。
  • 缺乏 A/B 测试能力: 想比较两个 Prompt 哪个更好?往往只能手动测试,效率低下且难以规模化。
  • 效果评估主观且低效: “感觉这个 Prompt 效果不错”——这种主观判断极其不可靠。没有量化指标,我们无法客观地衡量 Prompt 的优劣,更无法指导后续优化方向。

这些挑战正是 Prompt 工程 (Prompt Engineering) 致力于解决的问题。而 Prompt 工程框架 则是实现系统化、工程化 Prompt 开发的关键工具。它们带来的价值显而易见:

  • 标准化 (Standardization): 提供统一的模板语法和结构,规范 Prompt 的编写方式。
  • 模块化 (Modularity): 允许将 Prompt 分解为可复用的组件(如指令、示例、上下文占位符),便于组合和维护。
  • 可测试性 (Testability): 提供自动化评估工具和方法,用数据说话,量化 Prompt 的效果。
  • 可迭代性 (Iterability): 支持快速试验、评估不同 Prompt 版本,并基于评估结果进行有针对性的优化,形成高效的迭代循环。

简而言之,框架将 Prompt 开发从一种依赖灵感和个人经验的“艺术创作”,提升为一种有章可循、有据可依的“工程实践”。

3.2 构建可维护的指令:LangChain Prompt 模块与 LCEL

LangChain 作为领先的 LLM 应用开发框架,其 Prompt 模块 (langchain-core.prompts, langchain_community.prompts) 提供了强大的能力来构建、管理和组合 Prompts。
在这里插入图片描述

核心组件概览:

  • PromptTemplate: 用于处理简单的、基于 f-string 格式化语法的字符串 Prompt。适用于那些不需要复杂聊天结构的任务。
  • ChatPromptTemplate: (极其常用) 专为聊天模型设计,允许你构建包含一个或多个 MessagePromptTemplate 的 Prompt 列表。这些消息模板通常对应不同的角色:
    • SystemMessagePromptTemplate: 定义 LLM 的角色、行为指示或全局上下文。
    • HumanMessagePromptTemplate: 代表用户的输入。
    • AIMessagePromptTemplate: 代表 LLM 之前的回复(用于提供上下文或 Few-shot 示例)。
  • MessagesPlaceholder: 一个特殊的组件,用于在 ChatPromptTemplate 中为变量(通常是聊天历史 memory_key)动态插入一个消息列表。这是实现对话历史管理的关键。
  • FewShotPromptTemplate / FewShotChatMessagePromptTemplate: 允许你在 Prompt 中包含一些示例(输入/输出对),以引导模型更好地理解任务或遵循特定格式(Few-shot Learning)。

代码实战与深度解读 (Building Blocks):

目标:掌握使用 LangChain 构建灵活、可复用、适应不同场景的 Prompt 结构。

场景: 构建一个简单的聊天机器人 Prompt,它有一个系统指令,并能接收用户输入和聊天历史。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI # 假设使用 OpenAI
from langchain_core.output_parsers import StrOutputParser
import os
from dotenv import load_dotenv

load_dotenv()
# 确保 OPENAI_API_KEY 已设置

# 1. 定义聊天 Prompt 模板
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "你是一个乐于助人的AI助手,请简洁地回答用户的问题。"),
    # MessagesPlaceholder 用于插入聊天历史
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{user_input}") # 用户当前的输入
])

# 2. 准备输入数据 (模拟)
user_input = "LangChain 的 LCEL 是什么?"
# 模拟聊天历史 (通常由 Memory 组件管理)
chat_history = [
    ("human", "你好!"),
    ("ai", "你好!有什么可以帮你的吗?")
]

# 3. 格式化 Prompt (填充变量)
# formatted_prompt_value = prompt_template.invoke({
#     "chat_history": chat_history,
#     "user_input": user_input
# })
# print("--- Formatted Prompt Messages ---")
# print(formatted_prompt_value.to_messages())

# 4. 【关键】使用 LangChain Expression Language (LCEL) 组合 Prompt 与模型
# LCEL 是 LangChain 的核心,允许以声明式方式链式组合组件
# | 符号代表管道操作,将前一个组件的输出作为下一个组件的输入

print("\n--- Using LCEL to Chain Prompt and Model ---")
llm = ChatOpenAI(model="gpt-4o")
output_parser = StrOutputParser() # 解析 LLM 输出为字符串

# 定义处理链:Prompt -> LLM -> Output Parser
chain = prompt_template | llm | output_parser

# 执行链
response = chain.invoke({
    "chat_history": chat_history,
    "user_input": user_input
})

print("\n--- LLM Response via LCEL Chain ---")
print(response)

深度解读 (LCEL):

  • 为何 LCEL 是现代 LangChain 的核心? LCEL (LangChain Expression Language) 提供了一种极其强大且 Pythonic 的方式来组合 LLM 应用的各个组件(Prompt 模板、模型、输出解析器、检索器、工具等)。它使得构建复杂的处理链变得像写 Python 表达式一样简洁直观。
  • 简化连接: 在上面的例子中,prompt_template | llm | output_parser 清晰地表达了数据流:invoke 的输入字典首先被 prompt_template 处理成 PromptValue(消息列表),然后传递给 llm 进行调用,llm 的输出(AIMessage)再被 output_parser 解析成最终的字符串。LCEL 负责处理这些组件间的输入输出匹配和调用逻辑。这比旧版的链式写法(如 LLMChain 类)更灵活、更透明、更易于调试和自定义。我们将在后续章节中大量运用 LCEL 构建更复杂的 RAG 和 Agent 链。

3.3 量化与迭代:使用 Promptfoo 进行系统性评估

我们用 LangChain 精心构建了 Prompt,但如何知道它是否真的有效?相比于另一个版本,它是否更好?在不同的模型上表现如何?这时,我们就需要一个系统化的评估工具——Promptfoo

核心价值: Promptfoo 让你能够定义一系列测试用例(输入变量、预期输出断言),并自动运行这些测试用例来评估一个或多个 Prompt 在一个或多个 LLM 上的表现。它将 Prompt 优化从主观的“调参炼丹”转变为数据驱动的、可量化的工程过程
在这里插入图片描述

配置详解 (promptfooconfig.yaml):

Promptfoo 的核心是其配置文件(通常是 promptfooconfig.yaml),它定义了评估的所有方面:

  1. prompts: 定义要测试的 Prompt 文件路径或直接定义 Prompt 字符串。你可以列出多个 Prompt 文件,Promptfoo 会分别测试它们。
    prompts:
      - 'prompts/summary_v1.txt' # 指向一个包含 Prompt 的文件
      - 'prompts/summary_v2.txt'
      # 或者直接写 Prompt:
      # - 'Summarize the following: {{text}}'
    
  2. providers: 配置如何连接到 LLM API。可以直接提供 API Key,也可以指向一个执行脚本(比如调用 litellm 的脚本!),或者使用 openai:chat:gpt-4o 这样的快捷方式(需要设置相应环境变量)。
    providers:
      - openai:chat:gpt-4o # 使用 OpenAI gpt-4o
      - anthropic:completion:claude-3-haiku-20240307 # 使用 Anthropic Claude Haiku
      # 或者通过脚本调用 litellm
      # - id: litellm-provider
      #   exec: 'python call_litellm.py {{prompt}}'
    
  3. tests: 定义具体的测试用例。每个测试用例通常包含:
    • vars: 一个包含输入变量的字典,用于填充 Prompt 模板中的占位符。
    • assert: 一个或多个断言,用于检查 LLM 的输出是否符合预期。断言类型非常丰富,常用的包括:
      • contains / icontains: 包含/不区分大小写包含某个字符串。
      • equals: 完全等于某个值。
      • regex: 匹配正则表达式。
      • is-json: 输出是合法的 JSON。
      • javascript: 使用 JavaScript 代码进行更复杂的判断。
      • similar: 与预期输出在语义上相似(使用 embedding)。
      • llm-rubric: 让另一个 LLM 根据标准来评分。
      • webhook: 调用外部 webhook 进行校验。
    tests:
      - description: "测试简单摘要"
        vars:
          text: "LangChain是一个用于开发由语言模型驱动的应用程序的框架。"
        assert:
          - type: contains # 断言输出包含 'LangChain'
            value: LangChain
          - type: latency # 断言延迟小于 2000ms
            threshold: 2000
    
      - description: "测试需要提取JSON的场景"
        vars:
          user_request: "提取用户信息:姓名张三,年龄30"
        assert:
          - type: is-json # 断言输出是合法JSON
          - type: javascript # 使用JS检查JSON内容
            value: |
              (output) => {
                const data = JSON.parse(output);
                return data.name === '张三' && data.age === 30;
              }
    

在这里插入图片描述

代码实战与深度解读 (The Evaluation Loop):

目标:建立自动化流程来比较和改进 Prompt 性能。

Promptfoo 的使用通常在命令行进行:

  1. 安装 Promptfoo: npm install -g promptfoo (需要 Node.js 环境) 或使用 npx promptfoo@latest 临时运行。
  2. 编写配置文件: 创建 promptfooconfig.yaml 并根据你的需求填充 prompts, providers, tests
  3. 运行评估: 在包含配置文件的目录下运行命令:
    promptfoo eval
    
    • 比较不同 Prompt: 如果 prompts 列表包含多个 Prompt,Promptfoo 会在同一个测试用例上运行所有 Prompt,并展示结果对比。
    • 比较不同模型: 如果 providers 列表包含多个模型,Promptfoo 会使用每个模型运行所有测试用例,方便比较模型性能。
  4. 解读结果:
    • CLI 输出: Promptfoo 会在命令行中输出一个清晰的表格,显示每个 Prompt/Provider 组合在每个测试用例上的通过/失败情况、分数(基于通过率)、延迟、成本(如果配置了)等。失败的断言会明确指出原因。
    • Web UI (推荐): 运行 promptfoo view 会启动一个本地 Web 服务,以更直观、交互式的方式展示详细的评估结果,包括每个测试用elen的原始输入、输出、评分详情等。这对于深入分析失败案例非常有帮助。
  5. 核心循环:优化与再评估
    • 分析结果: 根据评估报告,识别哪些 Prompt 表现不佳,哪些测试用例失败率高,失败的原因是什么(断言不符?延迟太高?成本太贵?)。
    • 返回优化 (回到 3.2): 基于分析结果,回到你的 LangChain Prompt 模板代码中,调整指令、示例、结构或使用的变量。
    • 再次评估: 保存修改后的 Prompt 文件,再次运行 promptfoo eval
    • 重复: 不断重复这个“评估-分析-优化-再评估”的循环,直到 Prompt 的性能达到满意的水平。

集成考量:

对于追求极致工程化的团队,可以将 promptfoo eval 命令集成到 CI/CD (持续集成/持续部署) 流程中。例如,每次提交 Prompt 的修改时,自动运行评估,如果关键指标下降或失败率超过阈值,则阻止合并或部署,确保 Prompt 的质量得到持续监控。

本章小结与展望

在本章,我们完成了从 Prompt “手艺人”到 Prompt “工程师”的关键转变。我们掌握了:

  1. 使用 LangChain Prompt 模块LCEL构建结构化、可维护、可组合的 Prompt。
  2. 使用 Promptfoo 来对 Prompt 进行系统性、量化评估,并通过迭代优化循环持续改进其效果。

我们将 Prompt 开发从模糊的主观感受带入到了清晰的工程实践领域。拥有了精心设计和反复验证过的 Prompt,我们就具备了更精确地引导和控制 LLM 行为的基础能力。这为什么如此重要?因为在接下来的模块中,无论是构建需要精确信息检索的 RAG 系统,还是设计需要清晰指令和可靠工具调用的 Agent,高质量的 Prompt 工程都是不可或缺的基石。准备好迎接下一阶段的挑战了吗?让我们带着这身“工程化”的装备,深入探索 RAG 的世界!

内容同步在gzh:智语Bot

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

(initial)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值