LangChain应用与外部环境(主要通过用户输入)之间的交互边界是安全需要关注的重要方面。LLM应用通常处理非结构化的自然语言,这天生缺少传统软件输入所具备的严格模式。这种灵活性虽然强大,但也为恶意行为者操控应用行为提供了途径。因此,输入验证和清洗是减轻这些风险的基础做法,需要在潜在有害数据到达您的LLM、工具或后端系统之前进行。与传统应用中验证主要侧重于数据类型和格式不同,LLM环境下的验证还必须考虑语义内容及其可能以非预期方式影响语言模型或下游组件的潜力。LLM应用中输入验证的重要性未能正确验证和清洗输入可能导致LLM应用特有的多种安全漏洞:提示词注入: 这可以说是最突出的威胁。恶意输入可能包含旨在覆盖原始系统提示的指令,导致LLM忽略其预定任务、泄露敏感配置细节,或执行攻击者指定的动作。验证有助于检测或阻止注入攻击中常用的模式。不安全的工具参数: 如果您的LangChain代理使用与外部系统(数据库、API、文件系统)交互的工具,未经验证的输入作为参数传递给这些工具,可能直接导致经典的漏洞,例如SQL注入、跨站脚本(XSS)、服务器端请求伪造(SSRF)或远程代码执行(RCE)。清洗或严格验证工具输入是必要的。数据泄露: 精心设计的输入可能诱使LLM泄露其可访问的敏感数据,这些数据可能来自其训练数据(现代对齐技术下较不常见),或更重要的是,来自应用的上下文(例如,检索到的文档、对话历史)。拒绝服务(DoS): 极长、复杂或资源密集型输入可能会使LLM、相关工具或解析逻辑过载,导致成本过高或服务不可用。长度和复杂性检查是重要的预防措施。偏见或有害内容生成: 尽管通常通过模型对齐和输出过滤来解决,但输入验证可以作为初步检查,在提示被处理之前阻止其包含有毒、偏见或违反政策的内容。在何处应用验证和清洗有效的输入验证需要识别外部数据进入系统的所有位置,并尽早应用检查。LangChain应用中常见的集成点包括:初始用户界面/API边界: 用户输入(例如,聊天消息、表单数据)进入您应用的第一个位置。LLM调用之前: 在将由模板和用户输入构建的最终提示发送给LLM之前进行验证。工具执行之前: 非常重要。在参数传递给任何工具的执行逻辑之前,进行验证和清洗。数据加载/检索: 如果为RAG摄取外部文档,请在加载和分块过程中考虑验证或清洗内容,以防止数据源的间接提示注入。下图展示了一个涉及代理的请求流程中输入验证的常见位置:digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", color="#495057", fillcolor="#e9ecef", style="filled,rounded"]; edge [fontname="sans-serif", color="#495057"]; UserInput [label="用户输入"]; Validation1 [label="输入验证\n(长度, 格式, 基本模式)", shape=diamond, color="#f03e3e", fontcolor="#f03e3e"]; AgentExecutor [label="代理执行器\n(LangChain)"]; LLM [label="LLM调用"]; ToolSelection [label="工具选择\n(解析LLM输出)"]; Validation2 [label="工具参数验证\n(类型, 模式, 具体规则)", shape=diamond, color="#f03e3e", fontcolor="#f03e3e"]; ToolExecution [label="工具执行\n(API, 数据库等)"]; OutputProcessing [label="输出处理"]; FinalResponse [label="最终响应"]; UserInput -> Validation1; Validation1 -> AgentExecutor [label="有效"]; AgentExecutor -> LLM; LLM -> ToolSelection; ToolSelection -> Validation2; Validation2 -> ToolExecution [label="有效"]; ToolExecution -> AgentExecutor [label="工具结果"]; AgentExecutor -> OutputProcessing; OutputProcessing -> FinalResponse; Validation1 -> FinalResponse [label="无效 (拒绝/错误)", style=dashed]; Validation2 -> AgentExecutor [label="无效 (错误返回给代理)", style=dashed]; }此流程显示,验证在收到用户输入后立即应用,并在执行带有来自用户输入或LLM输出的潜在不安全参数的工具之前再次专门应用。验证与清洗技术选择适合特定输入和潜在风险的技术。通常需要多种技术的结合:类型检查: 确保输入数据符合预期的Python类型(例如,str、int、list、bool)。这是一项基本但重要的检查,特别是对于工具参数。长度限制: 限制字符串的最小和最大长度或集合的大小。这可以防止过于冗长的输入绕过其他检查或导致拒绝服务。MAX_INPUT_LENGTH = 1024 def validate_length(user_input: str): if not (0 < len(user_input) <= MAX_INPUT_LENGTH): # 抛出 ValueError,指示输入长度必须在指定范围内 raise ValueError(f"Input length must be between 1 and {MAX_INPUT_LENGTH} characters.") return user_input格式和模式验证 (Pydantic): 对于需要特定格式(电子邮件、URL)的结构化数据或输入,请使用Pydantic等库。Pydantic模型非常适合为工具输入定义预期模式,提供自动验证。LangChain与Pydantic良好集成,用于定义工具的args_schema。from pydantic import BaseModel, Field, validator import re class SearchToolSchema(BaseModel): query: str = Field(..., description="搜索查询", min_length=3, max_length=150) max_results: int = Field(default=5, gt=0, le=20) @validator('query') def query_safety_check(cls, v): # 示例:阻止明显的命令式结构(根据需要调整正则表达式) if re.search(r'[;&|`$()]', v): raise ValueError("查询包含潜在不安全字符。") # 示例:阻止特定关键词(谨慎使用,可能被绕过) if "DROP TABLE" in v.upper(): raise ValueError("检测到潜在有害的SQL关键词。") return v.strip() # 同时执行基本清洗允许列表(白名单): 准确定义什么是允许的(例如,特定字符、已知命令、枚举值)。这通常比阻止列表更安全,因为它拒绝任何未明确允许的内容。阻止列表(黑名单): 定义什么不允许(例如,password、admin等特定关键词,脚本标签<script>)。阻止列表因难以维护且攻击者易于通过编码或混淆技术绕过而臭名昭著。请谨慎使用它们作为第二道防线。正则表达式: 有助于强制执行特定模式(例如,仅字母数字、特定命令结构)或检测已知恶意模式(但请注意阻止列表的限制)。清洗: 修改输入以移除或中和潜在有害元素,而不是直接拒绝。请谨慎使用,因为它可能改变输入的含义。转义: 将特殊字符转换为安全的等效形式(例如,HTML转义>为>)。当输入可能在其他上下文(网页、SQL查询)中渲染时,这非常必要。对于SQL,始终优先使用参数化查询而非手动转义。剥离/移除: 删除不允许的字符或标签。例如,如果用户输入只应为纯文本,则从中移除HTML标签。规范化: 将输入转换为规范形式(例如,小写、移除多余空格、Unicode规范化)。这有助于使其他验证规则更有效。在LangChain中实现验证您可以将这些技术以多种方式集成到您的LangChain应用中:直接在工具代码中: 在自定义工具的_run或_arun方法中添加验证逻辑,或使用Pydantic的args_schema在工具调用时进行自动验证。from langchain.tools import BaseTool, StructuredTool from pydantic import BaseModel, Field from typing import Type # 使用Pydantic模式进行验证 class CalculatorInput(BaseModel): expression: str = Field(..., description="要评估的数学表达式") # 如果需要,在这里添加更具体的验证器 class SafeCalculatorTool(BaseTool): name = "safe_calculator" description = "安全地评估数学表达式。" args_schema: Type[BaseModel] = CalculatorInput def _run(self, expression: str): # 可以在此处进行与评估相关的进一步验证/清洗 # For example, use a safe evaluation library instead of eval() try: # 使用更安全的替代方案,如asteval或numexpr import numexpr # 如果需要,进一步清洗,例如,限制允许的函数/变量 result = numexpr.evaluate(expression).item() return result except Exception as e: return f"评估表达式时出错: {e}" # 如果需要,实现_arun用于异步在LCEL中使用RunnableLambda: 创建自定义验证或清洗函数,并将其封装为RunnableLambda组件,插入到您的LangChain表达式语言(LCEL)链中。from langchain_core.runnables import RunnableLambda, RunnablePassthrough from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI def validate_and_sanitize(input_data): # 假设 input_data 是一个包含 user_query 的字典 query = input_data.get("user_query", "") if len(query) > 500: # 查询超出最大长度500个字符 raise ValueError("Query exceeds maximum length of 500 characters.") # 基本清洗 sanitized_query = query.strip() # 在此处添加更多检查... input_data["user_query"] = sanitized_query return input_data # Pass through the potentially modified data validation_step = RunnableLambda(validate_and_sanitize) llm = ChatOpenAI(model="gpt-3.5-turbo") # 替换为您的LLM prompt = ChatPromptTemplate.from_template("回答用户的问题: {user_query}") # 早期包含验证的链 chain = ( RunnablePassthrough() # 从输入字典开始 | validation_step | prompt | llm # | output_parser ... ) # 调用示例 try: # result = chain.invoke({"user_query": " Tell me about LangChain. "}) # result_long = chain.invoke({"user_query": "A" * 1000}) # 这将引发 ValueError pass # 实际调用的占位符 except ValueError as e: print(f"验证失败: {e}") 自定义链组件: 对于涉及状态或多步骤的更复杂验证逻辑,请实现自定义的Runnable类。输入验证和清洗并非万灵药,但它们是LangChain应用纵深防御安全策略中的重要一层。通过仔细考虑潜在威胁并在重要的集成点应用适当的检查,您可以大大缩小LLM驱动系统的攻击面。请记住,根据涉及的数据和操作的具体上下文及敏感程度来调整您的验证规则。