各位专家、同仁,下午好!
今天,我们齐聚一堂,共同探讨一个在现代软件开发中日益凸显,且极具挑战性的议题:在 LangChain 工作流中,如何有效地混合使用“确定性代码”与“概率性大型语言模型 (LLM)”,并应对由此产生的“语义鸿沟”挑战。
随着人工智能技术的飞速发展,特别是大型语言模型(LLM)的崛起,我们正处在一个激动人心的时代。LLM以其卓越的自然语言理解和生成能力,正在深刻改变我们构建软件应用的方式。然而,LLM的本质是概率性的,它们的输出是基于统计预测而非精确计算。这与我们传统上赖以构建软件的确定性代码之间,存在着一道深刻的“语义鸿沟”。
LangChain作为一个强大的框架,旨在帮助开发者构建由LLM驱动的应用程序,它提供了连接LLM与外部数据源、工具的各种组件。它好比一座桥梁,试图连接两个截然不同的世界:一个是由严格逻辑、精确数据和可预测行为构成的确定性代码世界;另一个则是充满模糊性、创造性和不可预测性的概率性LLM世界。我们的任务,就是理解这座桥梁,并学习如何安全、高效地通过它。
一、 确定性代码的本质与特征
首先,让我们回顾一下我们所熟悉的确定性代码的世界。
确定性代码指的是在给定相同输入的情况下,总是产生相同输出的代码。它的行为是完全可预测的,其逻辑清晰、严格,并且遵循预定义的规则集。这是我们软件工程的基石,它带来了可靠性、可测试性和可调试性。
确定性代码的核心特征包括:
- 精确性 (Precision): 每个操作都有明确的定义和结果。
- 可预测性 (Predictability): 输入与输出之间存在明确的函数关系,结果可以被精确地预期。
- 可重复性 (Reproducibility): 相同的输入总是产生相同的输出,无论何时何地执行。
- 显式逻辑 (Explicit Logic): 程序的每一步逻辑都是由开发者明确编写和控制的。
- 强类型系统 (Strong Type Systems): 许多编程语言通过类型系统强制数据结构和操作的正确性,减少运行时错误。
- 错误处理 (Error Handling): 错误通常以异常、错误码等形式被明确捕获和处理。
优点:
- 可靠性: 关键业务逻辑、数据处理、金融计算等需要绝对准确的场景中不可或缺。
- 可测试性: 易于编写单元测试和集成测试,验证其正确性。
- 可调试性: 错误可以被精确地定位和修复。
- 控制力: 开发者对程序的行为拥有完全的控制权。
局限:
- 僵化性: 难以处理模糊、非结构化或高度变化的输入。
- 低泛化能力: 对于每一个新的业务规则或数据模式,都需要显式地编写代码。
- 高开发成本: 对于复杂的人类语言理解、创意生成等任务,编写确定性代码几乎是不可能的。
让我们看一个简单的Python例子,它展示了确定性代码的典型应用:计算税费和解析结构化数据。
import pandas as pd
from io import StringIO
import datetime
# 示例 1.1: 确定性函数 - 税费计算
def calculate_tax(amount: float, rate: float) -> float:
"""
根据给定的金额和税率计算税费。
这是一个确定性函数:相同的输入总是产生相同的输出。
"""
if not isinstance(amount, (int, float)) or not isinstance(rate, (int, float)):
raise TypeError("金额和税率必须是数字类型。")
if amount < 0 or rate < 0:
raise ValueError("金额和税率不能为负值。")
return round(amount * rate, 2) # 精确到两位小数,确保确定性
# 示例使用
price = 100.50
tax_rate = 0.08
total_tax = calculate_tax(price, tax_rate)
print(f"确定性税费计算: 价格={price}, 税率={tax_rate}, 税额={total_tax}") # 输出: 确定性税费计算: 价格=100.5, 税率=0.08, 税额=8.04
# 示例 1.2: 确定性数据解析和处理
def process_sales_data(csv_string: str) -> pd.DataFrame:
"""
解析CSV字符串并返回一个DataFrame。
这是一个确定性操作:相同的CSV字符串总是生成相同的DataFrame。
"""
# 使用StringIO模拟文件,实现确定性读取
df = pd.read_csv(StringIO(csv_string))
# 增加一个确定性列
df['total_price'] = df['quantity'] * df['unit_price']
return df
csv_data = """product_id,item_name,quantity,unit_price
101,Laptop,2,1200.50
102,Mouse,5,25.00
103,Keyboard,1,75.99
"""
sales_df = process_sales_data(csv_data)
print("n确定性销售数据处理结果:")
print(sales_df)
# 输出:
# product_id item_name quantity unit_price total_price
# 0 101 Laptop 2 1200.50 2401.00
# 1 102 Mouse 5 25.00 125.00
# 2 103 Keyboard 1 75.99 75.99
# 示例 1.3: 确定性日期格式化
def format_datetime(dt_obj: datetime.datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
"""
将datetime对象格式化为指定字符串。
如果输入和格式字符串相同,输出总是相同的。
"""
return dt_obj.strftime(fmt)
current_dt = datetime.datetime(2023, 10, 26, 14, 30, 0)
formatted_date = format_datetime(current_dt)
print(f"n确定性日期格式化: {formatted_date}") # 输出: 确定性日期格式化: 2023-10-26 14:30:00
这些例子清晰地展示了确定性代码的特点:它们是可预测、可控且高度可靠的。我们知道给定特定的输入,它们将产生精确的、我们期望的输出。
二、 概率性LLM的本质与特征
与确定性代码截然不同的是,大型语言模型(LLM)是概率性的。它们基于神经网络和海量的训练数据,通过预测下一个词元(token)的概率来生成文本。它们的行为不是由显式规则定义的,而是由训练数据中的统计模式和模型内部复杂的权重决定的。
概率性LLM的核心特征包括:
- 统计预测 (Statistical Prediction): LLM通过计算给定上下文中下一个词元的概率来生成文本。
- 上下文依赖 (Context Dependency): 输出高度依赖于输入的提示(prompt)及其上下文。
- 不确定性 (Non-determinism): 即使给定相同的输入,由于内部采样机制(如温度参数),LLM也可能产生略有不同的输出。
- 黑箱性质 (Black Box Nature): 模型的内部决策过程通常难以解释和追溯。
- 幻觉 (Hallucination): LLM可能生成听起来合理但实际上不准确或完全捏造的信息。
- 灵活性 (Flexibility): 能够理解和生成自然语言,处理非结构化和模糊的输入。
- 创造性 (Creativity): 能够生成新的、富有创意的文本,如诗歌、故事等。
优点:
- 自然语言理解和生成: 能够处理复杂的人类语言,进行语义分析、总结、翻译、问答等。
- 泛化能力: 能够处理训练数据中未曾见过的输入,并产生合理的回应。
- 创造性与灵活性: 适用于内容生成、创意写作、头脑风暴等任务。
- 低开发门槛: 相比于为每个特定任务编写规则,通过提示词(prompt)即可引导模型完成任务。
局限:
- 不可预测性: 难以保证输出的格式、内容和准确性始终符合预期。
- 缺乏精确推理: 虽然能进行文本操作,但缺乏真正的逻辑推理能力,容易犯简单错误。
- 幻觉与偏见: 可能产生不真实的信息或继承训练数据中的偏见。
- 难以调试: 当LLM产生错误时,很难追溯到具体的原因。
- 资源消耗: 调用LLM通常需要较高的计算资源和时间,且有API调用成本。
让我们通过LangChain来调用一个LLM,看看它的概率性行为。
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 确保OPENAI_API_KEY已设置在环境变量中
# os.environ["OPENAI_API_KEY"] = "sk-..." # 请替换为您的OpenAI API Key
# 示例 2.1: 基本的LangChain LLM调用
llm = ChatOpenAI(model="gpt-4o", temperature=0.7) # temperature > 0 引入随机性
output_parser = StrOutputParser()
# 定义一个简单的提示模板
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个乐于助人的AI助手。"),
("user", "{input}")
])
# 创建一个LangChain可运行链
chain = prompt | llm | output_parser
print("--- LLM 概率性响应示例 ---")
# 第一次调用
response1 = chain.invoke({"input": "用一句话描述蓝色的天空。"})
print(f"LLM Response 1: {response1}")
# 第二次调用,输入相同,但输出可能略有不同(由于temperature > 0)
response2 = chain.invoke({"input": "用一句话描述蓝色的天空。"})
print(f"LLM Response 2: {response2}")
# 示例 2.2: LLM的创造性与不确定性
poem_prompt = ChatPromptTemplate.from_messages([
("system", "你是一位富有诗意的诗人。"),
("user", "写一首关于秋天落叶的短诗。")
])
poem_chain = poem_prompt | llm | output_parser
print("n--- LLM 创意响应示例 (每次可能不同) ---")
poem_response1 = poem_chain.invoke({})
print(f"LLM 诗歌响应 1:n{poem_response1}")
poem_response2 = poem_chain.invoke({}) # 再次调用,诗歌内容可能不同
print(f"nLLM 诗歌响应 2:n{poem_response2}")
# 示例 2.3: LLM的模糊理解与潜在幻觉
factual_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个百科全书式的知识库。"),
("user", "世界上最高的山峰叫什么?高度是多少?谁是第一个登顶的人?")
])
factual_chain = factual_prompt | llm | output_parser
print("n--- LLM 事实查询示例 (可能包含幻觉或不精确) ---")
factual_response = factual_chain.invoke({})
print(f"LLM 事实响应:n{factual_response}")
# 注意:LLM通常能正确回答,但对于非常具体、非公开或最新信息,它可能“幻觉”出答案。
# 例如,如果问一个非常小众的公司的最新财报数据,LLM可能编造。
从这些例子中,我们可以清楚地看到LLM的灵活性和创造性,但同时也感受到其固有的不确定性。对于相同的输入,即使只是简单的事实描述,其措辞也可能略有不同。而对于创意性任务,每一次调用都可能产生全新的、独特的作品。
三、 语义鸿沟:挑战的根源
现在,我们已经清晰地认识到确定性代码和概率性LLM的各自特性。那么,“语义鸿沟”究竟是什么?
它指的是这两种范式在解释、处理和生成信息方式上的根本性不匹配。确定性代码期望严格的、结构化的、符合预定义模式的数据,并基于明确的规则进行操作。而LLM则在处理和生成非结构化、模糊、上下文相关的自然语言方面表现出色,其输出是概率性的,形式和内容都可能发生变化。
具体而言,语义鸿沟体现在以下几个方面:
- 数据类型与结构不匹配:
- 确定性代码: 需要严格的数据类型(整数、浮点数、布尔值、特定格式的日期、JSON对象、CSV文件等)。它依赖于强类型系统和明确的数据结构定义。
- 概率性LLM: 主要以文本形式进行输入和输出。即使我们尝试让LLM输出JSON,它也可能因为微小的格式错误而导致确定性解析器失败。
- 概念理解的偏差:
- 确定性代码: 对“语义”的理解是字面上的、精确的。例如,一个函数参数
status: str意味着它必须是一个字符串,而不能是None,除非明确允许。 - 概率性LLM: 对“语义”的理解是基于上下文和统计关联的。它可能理解“status”的多种含义(例如,订单状态、系统状态、心理状态),并根据上下文进行推断。这种灵活性在LLM内部是优势,但在与确定性代码交互时可能导致误解。
- 确定性代码: 对“语义”的理解是字面上的、精确的。例如,一个函数参数
- 错误处理机制的差异:
- 确定性代码: 通过异常、错误码、断言等机制明确指示错误,并且通常可以精确追溯到错误的根源。
- 概率性LLM: 错误可能表现为“幻觉”、不相关输出、格式偏差等,这些错误很难被标准化捕获,也难以精确调试。一个看似合理的LLM响应可能在业务逻辑层面是完全错误的。
- 控制流与决策逻辑:
- 确定性代码: 程序的执行流程由
if/else、for/while、函数调用等明确控制。 - 概率性LLM: LLM在Agent模式下可以“自主”决定下一步行动,包括选择调用哪个工具。这种决策是概率性的,可能不总是最优或符合预期的,给确定性流程带来了不确定性。
- 确定性代码: 程序的执行流程由
为了更直观地理解这种差异,我们可以通过一个表格进行对比:
| 特性 | 确定性代码 | 概率性LLM |
|---|---|---|
| 输出 | 精确、可预测、严格符合定义的数据类型(如int, float, JSON, XML) | 文本、可变、基于概率的词元序列 |
| 逻辑 | 显式、算法、规则驱动,每一步都可追溯 | 隐式、统计、模式识别,内部决策过程不透明 |
| 错误 | 异常、明确的错误码、可调试,易于定位 | 幻觉、不相关、模糊、难以追溯,可能导致悄无声息的逻辑错误 |
| 输入 | 严格类型、结构化数据(如API参数、数据库记录) | 自然语言、上下文、非结构化或半结构化 |
| 可解释性 | 高,每一步都可追溯 | 低,黑箱,难以解释生成某个输出的原因 |
| 灵活性 | 低,需要显式编程所有情况,难以处理未预见的输入 | 高,能泛化、处理新颖或模糊的输入,理解上下文 |
| 性能 | 高效、低延迟 (取决于算法复杂度) | 较高延迟、计算密集、API调用成本 |
| 安全性 | 可通过严谨的测试和审计保证 | 存在偏见、幻觉、数据泄露等风险,难以完全控制 |
这种语义鸿沟是我们在构建由LLM驱动的应用程序时必须面对的核心挑战。如果处理不当,它可能导致系统不稳定、数据错误、逻辑漏洞,甚至在关键业务场景中产生灾难性后果。
四、 LangChain工作流中弥合语义鸿沟的策略与技术
幸运的是,LangChain作为一个专门为LLM应用设计的框架,提供了丰富的工具和模式来帮助我们桥接这道鸿沟。核心思想是:在LLM和确定性代码之间建立清晰、可靠的接口,并引入鲁棒的错误处理和验证机制。
我们将从几个关键方面来探讨这些策略和技术:
A. 结构化输出解析 (Structured Output Parsing)
这是弥合鸿沟最直接、最常见的挑战之一。LLM擅长生成文本,但我们的确定性代码通常需要结构化的数据(如JSON、Pydantic对象、XML等)才能继续处理。
需求: 将LLM的自由文本输出转换为确定性代码可处理的、具有明确数据类型和结构的格式。
核心技术:
-
Prompt Engineering for Structure (提示工程指定结构):
- 通过系统指令(System Prompt)明确告诉LLM期望的输出格式。
- 提供Few-shot示例,向LLM展示正确的格式。
- 在Prompt中直接嵌入JSON Schema或Pydantic模型的描述,让LLM理解并尝试遵循。
-
LangChain 的输出解析器 (Output Parsers):
- LangChain提供了多种内置解析器,可以将LLM的文本输出转换为Python对象。
JsonOutputParser:将JSON字符串解析为Python字典。PydanticOutputParser:基于Pydantic模型定义期望的输出结构,并尝试将LLM输出解析为该模型的实例。这提供了强大的数据验证功能。StructuredOutputParser:使用ResponseSchema列表定义期望的结构。OutputFixingParser:在第一次解析失败时,尝试让LLM修复其自身的错误输出,增加健壮性。
让我们通过代码示例来详细说明。
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser, StrOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError
from langchain_core.exceptions import OutputParserException
# 确保OPENAI_API_KEY已设置在环境变量中
# os.environ["OPENAI_API_KEY"] = "sk-..."
llm = ChatOpenAI(model="gpt-4o", temperature=0) # 尽量降低温度以获得更稳定的结构化输出
# 示例 3.1: 使用 PydanticOutputParser 将 LLM 输出解析为 Pydantic 对象
class PersonInfo(BaseModel):
"""关于一个人的结构化信息。"""
name: str = Field(description="人的全名")
age: int = Field(description="人的年龄")
city: str = Field(description="人居住的城市")
occupation: str = Field(description="人的职业", default="未知") # 可以有默认值
# 创建一个 PydanticOutputParser 实例
pydantic_parser = JsonOutputParser(pydantic_object=PersonInfo)
# 获取 Pydantic 模型自动生成的格式指令
format_instructions = pydantic_parser.get_format_instructions()
print("Pydantic 模型自动生成的格式指令:n", format_instructions)
# 定义一个提示模板,包含格式指令
person_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个信息提取专家。请从用户提供的文本中提取人物信息,并严格按照以下 JSON 格式输出:n{format_instructions}"),
("user", "{text}")
])
# 构建链
pydantic_chain = person_prompt | llm | pydantic_parser
user_text1 = "张三今年30岁,住在北京,是一名软件工程师。"
try:
person_data1 = pydantic_chain.invoke({"text": user_text1, "format_instructions": format_instructions})
print(f"n成功解析为 PersonInfo 对象: {person_data1}")
print(f"姓名: {person_data1.name}, 年龄: {person_data1.age}, 城市: {person_data1.city}, 职业: {person_data1.occupation}")
print(f"类型: {type(person_data1)}")
except (OutputParserException, ValidationError) as e:
print(f"解析失败: {e}")
user_text2 = "李四,45岁,上海人。他是一名医生。"
try:
person_data2 = pydantic_chain.invoke({"text": user_text2, "format_instructions": format_instructions})
print(f"n成功解析为 PersonInfo 对象: {person_data2}")
print(f"姓名: {person_data2.name}, 年龄: {person_data2.age}, 城市: {person_data2.city}, 职业: {person_data2.occupation}")
except (OutputParserException, ValidationError) as e:
print(f"解析失败: {e}")
# 示例 3.2: 使用 OutputFixingParser 处理潜在的错误输出
from langchain_core.output_parsers import OutputFixingParser
class ProductDetails(BaseModel):
product_name: str = Field(description="产品名称")
price: float = Field(description="产品价格")
currency: str = Field(description="价格的货币单位,如 USD, EUR, CNY")
in_stock: bool = Field(description="产品是否有库存")
product_parser = JsonOutputParser(pydantic_object=ProductDetails)
# OutputFixingParser 会在解析失败时,将原始LLM输出和错误信息再次发送给LLM,让其尝试修复
fix_parser = OutputFixingParser.from_llm(parser=product_parser, llm=llm)
product_prompt = ChatPromptTemplate.from_messages([
("system", "提取产品信息。严格按照以下 JSON 格式输出:n{format_instructions}"),
("user", "{text}")
])
# 模拟一个 LLM 可能输出的略有格式错误的 JSON 字符串(例如,price是字符串而非浮点数)
# 正常情况下,llm_main_chain = product_prompt | llm | product_parser
# 这里我们直接模拟LLM输出,然后用fix_parser来处理
malformed_llm_output_str = '{"product_name": "智能手机", "price": "999", "currency": "USD", "in_stock": "true"}' # price is string, in_stock is string
print("n--- 尝试使用 OutputFixingParser 修复 LLM 的错误输出 ---")
try:
# 假设 LLM 直接返回了 malformed_llm_output_str
# fix_parser 内部会先尝试用 product_parser 解析,失败后会调用 LLM 修复
fixed_product_data = fix_parser.parse(malformed_llm_output_str)
print(f"成功修复并解析为 ProductDetails 对象: {fixed_product_data}")
print(f"产品名称: {fixed_product_data.product_name}, 价格: {fixed_product_data.price} ({type(fixed_product_data.price)}), 库存: {fixed_product_data.in_stock}")
except (OutputParserException, ValidationError) as e:
print(f"即使使用 OutputFixingParser 也未能修复和解析: {e}")
except Exception as e:
print(f"发生未知错误: {e}")
# 示例 3.3: 使用 StructuredOutputParser 处理更通用的结构
from langchain_core.output_parsers import StructuredOutputParser, ResponseSchema
# 定义 ResponseSchema 列表
response_schemas = [
ResponseSchema(name="answer", description="用户问题的答案"),
ResponseSchema(name="source", description="答案的来源,应为一个网站URL")
]
structured_parser = StructuredOutputParser.from_response_schemas(response_schemas)
structured_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个信息检索助手。请回答用户的问题,并提供一个来源URL。"
"输出格式请严格遵循:n{format_instructions}"),
("user", "{question}")
])
structured_chain = structured_prompt | llm | structured_parser
structured_format_instructions = structured_parser.get_format_instructions()
print("n--- StructuredOutputParser 示例 ---")
question = "梵高最著名的画作是什么?"
try:
structured_response = structured_chain.invoke({"question": question, "format_instructions": structured_format_instructions})
print(f"结构化响应: {structured_response}")
print(f"答案: {structured_response['answer']}")
print(f"来源: {structured_response['source']}")
print(f"类型: {type(structured_response)}")
except (OutputParserException, ValidationError) as e:
print(f"结构化解析失败: {e}")
通过这些解析器,我们可以将LLM的自由文本输出“驯服”成确定性代码可以理解和处理的结构化数据,极大地缩小了语义鸿沟。Pydantic模型在这里扮演了关键角色,它不仅定义了数据结构,还提供了运行时验证,确保数据的完整性和正确性。
B. 输入验证与预处理 (Input Validation and Pre-processing)
在将数据传递给LLM之前,进行严格的输入验证和预处理同样重要。LLM虽然强大,但它们的性能对输入质量高度敏感。不干净、不一致或恶意的输入可能导致LLM产生错误、不相关甚至有害的输出(例如,提示注入攻击)。
需求: 确保输入LLM的数据是干净、有效、安全且符合预期的,从而提高LLM输出的质量和可靠性。
核心技术:
- 数据清洗 (Data Cleaning):
- 移除无关信息(如HTML标签、多余空格)。
- 标准化格式(日期、数字、文本大小写)。
- 去除敏感信息(PII,个人身份信息)或进行脱敏处理。
- 模式验证 (Schema Validation):
- 使用Pydantic模型或JSON Schema对输入数据进行结构和类型验证。
- 确保输入符合预期的格式和约束,避免将不正确的数据发送给LLM。
- 上下文接地 (Contextual Grounding):
- 通过检索增强生成(RAG)从确定性知识库中获取相关且准确的信息,作为LLM的上下文输入。
- 通过数据库查询或API调用,将外部确定性数据注入到Prompt中。
- 这有助于限制LLM的“想象空间”,使其基于事实而非幻觉生成内容。
代码示例 4:Pydantic输入验证与简单预处理
from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError
from typing import List, Optional
import re
# 示例 4.1: 使用 Pydantic 进行输入数据验证
class UserQuery(BaseModel):
"""用户提交的查询请求的结构化表示。"""
query_text: str = Field(min_length=10, max_length=500, description="用户的主要查询文本")
category: Optional[str] = Field(default=None, description="查询的类别,例如 '技术', '销售', '支持'")
priority: int = Field(default=1, ge=1, le=5, description="查询优先级 (1-5,5最高)")
tags: List[str] = Field(default_factory=list, description="与查询相关的标签")
def validate_and_preprocess_query(raw_data: dict) -> UserQuery:
"""
验证并预处理原始用户查询数据。
"""
print(f"n--- 正在处理原始数据: {raw_data} ---")
try:
# 1. Pydantic 模式验证
validated_query = UserQuery(**raw_data)
print(f"Pydantic 验证成功: {validated_query}")
# 2. 简单的数据清洗/标准化
# 清除查询文本中的多余空白字符
validated_query.query_text = re.sub(r's+', ' ', validated_query.query_text).strip()
# 将所有标签转换为小写
validated_query.tags = [tag.lower() for tag in validated_query.tags]
print(f"预处理后数据: {validated_query}")
return validated_query
except ValidationError as e:
print(f"输入验证失败: {e}")
raise # 重新抛出异常,让上层调用者处理
except Exception as e:
print(f"预处理过程中发生未知错误: {e}")
raise
# 测试用例
valid_raw_data = {
"query_text": " 我需要查找关于 最新的AI伦理指导原则 ",
"category": "技术",
"priority": 4,
"tags": ["AI", "伦理", "政策"]
}
try:
processed_query1 = validate_and_preprocess_query(valid_raw_data)
# 经过验证和预处理的数据可以安全地用于构建 LLM prompt
# 例如: prompt_template = ChatPromptTemplate.from_messages([..., ("user", "处理查询:{query_text},类别:{category}")])
# chain.invoke({"query_text": processed_query1.query_text, "category": processed_query1.category})
except ValidationError:
pass
invalid_short_query = {
"query_text": "AI", # 小于最小长度
"priority": 2
}
try:
processed_query2 = validate_and_preprocess_query(invalid_short_query)
except ValidationError:
pass
invalid_priority_query = {
"query_text": "请帮我写一份报告草稿,关于市场趋势分析。",
"priority": 10 # 超过最大值
}
try:
processed_query3 = validate_and_preprocess_query(invalid_priority_query)
except ValidationError:
pass
# 示例 4.2: 模拟上下文接地 - 从确定性数据源检索信息
def retrieve_product_info(product_id: str) -> Optional[dict]:
"""
模拟从数据库中检索产品信息 (确定性操作)。
"""
db_data = {
"P001": {"name": "Laptop Pro", "price": 1500.00, "description": "高性能笔记本电脑,适合专业人士。"},
"P002": {"name": "Wireless Mouse", "price": 35.00, "description": "人体工学无线鼠标。"},
}
return db_data.get(product_id)
def create_llm_prompt_with_grounding(user_request: str, product_id: Optional[str] = None) -> str:
"""
根据用户请求和检索到的产品信息构建 LLM prompt。
"""
context = ""
if product_id:
product_info = retrieve_product_info(product_id)
if product_info:
context = f"以下是产品信息:{product_info['name']},价格 {product_info['price']},描述:{product_info['description']}n"
else:
context = f"未找到产品ID '{product_id}' 的信息。n"
full_prompt = f"请根据以下信息和用户请求生成回应:n{context}用户请求:{user_request}"
return full_prompt
print("n--- 上下文接地示例 ---")
user_query_product = "请介绍一下P001产品。"
llm_input_with_context = create_llm_prompt_with_grounding(user_query_product, "P001")
print(f"LLM输入 (带产品信息): n{llm_input_with_context}")
# 此时,LLM会基于 P001 的确定性数据来生成回答,减少幻觉
user_query_no_product = "请推荐一款适合程序员的笔记本电脑。"
llm_input_no_context = create_llm_prompt_with_grounding(user_query_no_product)
print(f"nLLM输入 (无产品信息): n{llm_input_no_context}")
# 此时,LLM会依赖其通用知识来回答
通过输入验证和预处理,我们能够确保发送给LLM的数据是高质量的,从而提高LLM生成输出的准确性和相关性,并降低安全风险。上下文接地尤其关键,它将LLM的概率性推理能力与确定性事实相结合,是构建可靠RAG系统的基础。
C. 错误处理与健壮性 (Error Handling and Robustness)
LLM的概率性意味着错误是不可避免的。无论是API调用失败、输出格式不正确,还是LLM生成了逻辑错误的“幻觉”内容,我们都需要在确定性代码中建立强大的错误处理和回退机制。
需求: 在LLM工作流中,能够优雅地处理各种预期和非预期的错误,确保系统的稳定性和用户体验。
核心技术:
try-except块:- 捕获LangChain组件可能抛出的各种异常(如
OutputParserException、API调用错误)。 - 对特定类型的错误进行精细化处理。
- 捕获LangChain组件可能抛出的各种异常(如
- 回退机制 (Fallback Mechanisms):
- 更简单的LLM: 当主LLM(如GPT-4o)因复杂性或成本原因失败时,尝试调用一个更小、更便宜、可能更稳定的模型(如GPT-3.5-turbo)来完成简化的任务。
- 确定性逻辑: 当LLM完全无法产生可用输出时,回退到预定义的确定性逻辑,例如返回一个默认值、一个预设模板,或触发人工审核。
- 重试机制: 对于瞬时网络错误或LLM偶尔的格式错误,可以尝试重试调用。LangChain的
with_retry()方法可以用于Runnable。
- 日志与监控:
- 记录LLM的输入、输出、耗时以及任何发生的错误。
- 通过监控系统识别错误模式和频率,以便及时调整Prompt或模型。
- LangSmith等工具能提供强大的可观测性。
代码示例 5:带有回退机制的错误处理
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser, StrOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError
from langchain_core.exceptions import OutputParserException
from typing import Optional, Dict, Any
# 确保OPENAI_API_KEY已设置在环境变量中
# os.environ["OPENAI_API_KEY"] = "sk-..."
# 定义一个用于任务提取的 Pydantic 模型
class TaskDetails(BaseModel):
task_name: str = Field(description="任务的名称或简短描述")
due_date: Optional[str] = Field(default=None, description="任务的截止日期,例如 '明天', '下周五', '2023-12-31'")
priority: int = Field(ge=1, le=5, description="任务的优先级,1为最低,5为最高")
status: str = Field(default="待办", description="任务的当前状态,例如 '待办', '进行中', '已完成'")
task_parser = JsonOutputParser(pydantic_object=TaskDetails)
format_instructions_task = task_parser.get_format_instructions()
# 主 LLM:用于复杂任务,可能更昂贵或更易出错
llm_main = ChatOpenAI(model="gpt-4o", temperature=0.5)
# 回退 LLM:用于简单任务或主 LLM 失败时,可能更便宜、更稳定
llm_fallback = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
task_prompt_template = ChatPromptTemplate.from_messages([
("system", "你是一个任务管理助手。请从用户请求中提取任务详情,并严格按照以下 JSON 格式输出:n{format_instructions}"),
("user", "{request}")
])
# 创建主 LLM 链
main_task_chain = task_prompt_template | llm_main | task_parser
# 创建回退 LLM 链
fallback_task_chain = task_prompt_template | llm_fallback | task_parser
def extract_task_with_fallback(user_request: str) -> TaskDetails:
"""
尝试使用主 LLM 提取任务详情,失败时回退到次级 LLM,
如果再次失败,则回退到确定性默认值。
"""
print(f"n--- 正在处理请求: '{user_request}' ---")
# 1. 尝试使用主 LLM
try:
print("尝试使用主 LLM (gpt-4o) 提取任务...")
task_details = main_task_chain.invoke({"request": user_request, "format_instructions": format_instructions_task})
print(f"主 LLM 成功提取: {task_details}")
return task_details
except (OutputParserException, ValidationError) as e:
print(f"主 LLM 解析或验证失败: {e}")
print("回退到次级 LLM (gpt-3.5-turbo)...")
# 2. 尝试使用回退 LLM
try:
task_details = fallback_task_chain.invoke({"request": user_request, "format_instructions": format_instructions_task})
print(f"次级 LLM 成功提取: {task_details}")
return task_details
except (OutputParserException, ValidationError) as fe:
print(f"次级 LLM 解析或验证失败: {fe}")
print("回退到确定性默认任务详情...")
# 3. 回退到确定性默认值
return TaskDetails(task_name=f"处理请求 '{user_request}'", due_date="未知", priority=3, status="待办")
except Exception as e:
# 捕获其他如 API 调用错误、网络错误等
print(f"LLM 调用发生通用错误: {e}")
print("回退到确定性默认任务详情...")
return TaskDetails(task_name=f"处理请求 '{user_request}'", due_date="未知", priority=3, status="待办")
# 测试用例
# 1. 正常情况,主 LLM 成功
task1 = extract_task_with_fallback("请创建一个任务,明天下午5点前完成项目报告,优先级为5。")
print(f"最终任务详情: {task1}")
# 2. LLM 输出格式有误(模拟,实际上可能需要更复杂的prompt才能让LLM出错)
# 假设 LLM 因为某种原因输出 '{"task_name": "Buy groceries", "priority": "high"}' (priority是字符串)
# 我们可以模拟这种失败,或者设计一个故意让LLM难以遵循格式的prompt
# 对于本例,我们假设 llm_main 偶尔会犯错
class MalformedTaskDetails(BaseModel):
task_name: str
priority: str # 故意设计为字符串,Pydantic会报错
malformed_task_parser = JsonOutputParser(pydantic_object=MalformedTaskDetails)
malformed_format_instructions = malformed_task_parser.get_format_instructions()
# 模拟一个会出错的链
malforming_chain = ChatPromptTemplate.from_messages([
("system", "请提取任务,但故意将优先级输出为字符串:n{format_instructions}"),
("user", "{request}")
]) | llm_main | malformed_task_parser # 注意这里我们暂时将解析器换成会报错的
def demonstrate_parsing_failure(request: str):
try:
print(f"n--- 模拟主 LLM 解析失败的场景: '{request}' ---")
# 实际场景中,这里的 `main_task_chain` 会因为 LLM 的不准确输出而抛出 OutputParserException
# 为了演示,我们直接用一个会失败的解析器
# task_details = main_task_chain.invoke({"request": request, "format_instructions": format_instructions_task})
task_details = malforming_chain.invoke({"request": request, "format_instructions": malformed_format_instructions})
print(f"意外成功: {task_details}")
except (OutputParserException, ValidationError) as e:
print(f"如预期,主 LLM 解析失败: {e}")
# 这里实际上会触发 extract_task_with_fallback 中的回退逻辑
demonstrate_parsing_failure("购买食材,高优先级。")
# 3. 复杂请求,主 LLM 可能处理不好,回退 LLM 也许能简化处理
task3 = extract_task_with_fallback("我需要一个任务来组织下个月的团队建设活动,具体细节稍后确定,现在先标记为中等优先级。")
print(f"最终任务详情: {task3}")
# 4. 极端情况,LLM 可能无法理解或生成有效输出,最终回退到确定性默认
task4 = extract_task_with_fallback("gkjhgjkhgkhgkjhgkjhgkjh") # 随机输入
print(f"最终任务详情: {task4}")
通过分层的错误处理和回退策略,我们可以显著提高LLM应用的健壮性。即使LLM未能产生完美输出,系统也能以可控的方式响应,避免崩溃或提供完全无用的信息。
D. 编排与控制流 (Orchestration and Control Flow)
在复杂的应用中,我们不仅仅是调用LLM,更需要将LLM的输出与确定性代码的执行、外部工具的调用结合起来。LangChain的表达式语言(LCEL)和Agent机制正是为此而生。
需求: 在一个工作流中,有序地协调LLM和确定性代码的执行,管理数据流,并让LLM能够根据需要调用确定性工具。
核心技术:
- LangChain Chains (LCEL – LangChain Expression Language):
RunnableSequence:按顺序执行多个组件,将一个组件的输出作为下一个组件的输入。RunnableParallel:并行执行多个组件。RunnablePassthrough:将输入原样传递给下一个组件,或将特定键传递给下游组件。- 这些LCEL组件允许我们构建复杂而灵活的工作流,清晰地定义数据如何在LLM和确定性代码之间流动。
- 自定义工具 (Custom Tools):
- 将确定性代码(例如,数据库查询、API调用、文件操作、复杂的业务逻辑)封装成LLM可以理解和调用的“工具”。
- 通过
@tool装饰器(或继承BaseTool),我们可以很容易地定义这些工具。
- 代理 (Agents):
- Agent是LangChain中一个强大的概念,它允许LLM根据用户的输入和可用的工具,自主地决定执行哪一步操作。
- LLM会观察输入,思考下一步行动,选择一个工具,提供输入,然后观察结果,这个过程可以迭代进行。Agent是连接LLM(决策者)与确定性工具(执行者)的关键。
代码示例 6:使用自定义工具的LangChain Agent
import os
import datetime
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.tools import tool
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from typing import Dict, Any
# 确保OPENAI_API_KEY已设置在环境变量中
# os.environ["OPENAI_API_KEY"] = "sk-..."
llm_agent = ChatOpenAI(model="gpt-4o", temperature=0) # Agent通常使用较低的温度以获得更稳定的决策
# 示例 6.1: 定义确定性工具 (使用 @tool 装饰器)
@tool
def get_current_utc_time(format: str = "%Y-%m-%d %H:%M:%S") -> str:
"""
返回当前的 UTC 日期和时间。
参数:
format (str): 日期时间格式字符串,默认为 "%Y-%m-%d %H:%M:%S"。
"""
return datetime.datetime.now(datetime.timezone.utc).strftime(format)
@tool
def calculate_simple_interest(principal: float, rate: float, years: int) -> float:
"""
计算给定本金、年利率和年限的简单利息。
参数:
principal (float): 本金。
rate (float): 年利率 (例如,0.05 代表 5%)。
years (int): 年限。
"""
if principal < 0 or rate < 0 or years < 0:
raise ValueError("本金、利率和年限必须是非负数。")
return round(principal * rate * years, 2)
@tool
def check_inventory_level(product_id: str) -> Dict[str, Any]:
"""
查询特定产品的库存水平 (模拟数据库查询)。
参数:
product_id (str): 产品的唯一标识符。
"""
# 这是一个模拟的确定性数据库查询
inventory_data = {
"P001": {"name": "Laptop Pro", "stock": 150, "location": "Warehouse A"},
"P002": {"name": "Wireless Mouse", "stock": 500, "location": "Warehouse B"},
"P003": {"name": "Mechanical Keyboard", "stock": 0, "location": "N/A"},
}
info = inventory_data.get(product_id, {"name": "未知产品", "stock": -1, "location": "N/A"})
if info["stock"] == -1:
return {"status": "error", "message": f"未找到产品ID: {product_id}"}
elif info["stock"] == 0:
return {"status": "out_of_stock", "product_name": info["name"], "stock": 0}
else:
return {"status": "in_stock", "product_name": info["name"], "stock": info["stock"], "location": info["location"]}
# 将所有工具集中
tools = [get_current_utc_time, calculate_simple_interest, check_inventory_level]
# 示例 6.2: 创建 Agent
# Agent 的 Prompt,指导 LLM 如何思考和使用工具
agent_prompt_template = PromptTemplate.from_template("""
你是一个有用的助手,可以回答问题和执行任务。
你有权访问以下工具:
{tools}
使用以下格式进行思考和行动:
Question: 要回答的输入问题
Thought: 你应该总是思考接下来要做什么
Action: 要采取的行动,应该是 [{tool_names}] 中的一个
Action Input: 行动的输入 (JSON格式)
Observation: 行动的结果
... (这个 Thought/Action/Action Input/Observation 循环可以重复多次)
Thought: 我现在知道最终答案了
Final Answer: 最终答案
开始!
Question: {input}
{agent_scratchpad}
""")
# 创建 React Agent
agent = create_react_agent(llm_agent, tools, agent_prompt_template)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
print("n--- Agent 演示: 获取当前时间 ---")
agent_executor.invoke({"input": "现在几点了?"})
# LLM 会思考,决定调用 get_current_utc_time 工具,然后返回结果。
print("n--- Agent 演示: 计算利息 ---")
agent_executor.invoke({"input": "计算1000美元本金,年利率5%,3年的简单利息。"})
# LLM 会从问题中提取数字,决定调用 calculate_simple_interest 工具,并提供正确参数。
print("n--- Agent 演示: 检查库存 ---")
agent_executor.invoke({"input": "P001产品还有库存吗?"})
# LLM 会决定调用 check_inventory_level 工具。
print("n--- Agent 演示: 检查缺货产品 ---")
agent_executor.invoke({"input": "P003产品库存情况如何?"})
print("n--- Agent 演示: 处理未知产品 ---")
agent_executor.invoke({"input": "产品X999的库存是多少?"})
print("n--- Agent 演示: 纯知识问题 (不使用工具) ---")
agent_executor.invoke({"input": "日本的首都是哪里?"})
# LLM 会直接回答,因为没有工具可以回答这个问题。
通过Agent和自定义工具,我们将LLM的“大脑”与确定性代码的“四肢”连接起来。LLM负责理解意图、规划和决策,而确定性工具则负责精确、可靠地执行具体任务。这是一种非常强大的模式,允许我们构建能够执行复杂、多步骤任务的智能系统,同时保持对关键业务逻辑的确定性控制。
E. 语义对齐与接地 (Semantic Alignment and Grounding)
LLM虽然拥有庞大的知识,但其对特定领域术语、内部数据和业务规则的理解可能不准确或存在偏差。为了确保LLM与确定性系统之间的语义一致性,我们需要将LLM“接地”到我们自己的知识和数据上。
需求: 确保LLM对特定领域概念、术语和事实的理解与确定性系统保持一致,避免幻觉和误解。
核心技术:
- RAG (Retrieval Augmented Generation – 检索增强生成):
- 这是最流行的“接地”技术。在生成回答之前,系统会从一个确定性知识库(如文档、数据库、内部wiki)中检索相关信息。
- 这些检索到的信息被作为上下文提供给LLM,LLM基于这些“事实”生成回答。
- 这样,LLM的回答就被“锚定”在真实、最新的数据上,大大减少了幻觉。
- Embedding (嵌入):
- 将确定性数据(如文本段落、数据库记录、产品描述)转换为高维向量(嵌入)。
- 通过这些嵌入,可以进行语义搜索,找到与用户查询最相关的确定性信息,作为RAG的输入。
- Ontologies/Taxonomies (本体/分类法):
- 为LLM提供结构化的术语表、概念关系和领域知识图谱。
- 在Prompt中明确定义关键术语及其含义,或将这些本体作为LLM可查询的工具。
代码示例 7:基本RAG工作流
import os
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import