ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LangChain提示词工程:从字符串拼接升级到组合流水线开发范式

LangChain提示词工程:从字符串拼接升级到组合流水线开发范式 1. 项目概述从“字符串拼接”到“组合流水线”的范式跃迁如果你正在用LangChain构建大语言模型应用并且还在用Python的f-string或者简单的字符串拼接来组装提示词那么这篇文章就是为你准备的。我经历过那个阶段为了给模型一个清晰的指令代码里充斥着各种{variable}的占位符和操作符调试起来像在玩“大家来找茬”一旦需求变动改起来更是牵一发而动全身。这本质上还是手工作坊式的开发。直到我系统性地将提示词模板升级为“组合流水线”模式整个开发体验和应用的健壮性才发生了质变。这不仅仅是换个写法而是一种工程思维的转变。简单来说提示词模板Prompt Template是LangChain提供的一个基础组件它帮你把动态变量和固定指令模板分离开避免了硬编码。而组合流水线Composition Pipeline则是更高阶的用法它通过将多个模板、处理步骤如格式转换、示例选择、甚至其他LangChain组件如检索器、输出解析器串联起来形成一个可复用、可测试、可维护的工作流。为什么这个转变如此重要想象一下一个复杂的问答系统提示词可能包含系统角色设定、从知识库检索到的上下文、用户的历史对话、以及当前问题。如果全靠拼接代码会臃肿且脆弱。而流水线模式允许你将“构建系统指令”、“注入上下文”、“格式化历史记录”和“组装最终提问”这几个步骤解耦每个步骤独立可控就像工厂的装配线清晰、高效且易于扩展。这正是LangChain设计哲学的核心——通过组合Composition来构建复杂应用。接下来我将带你从最基础的模板使用一步步拆解如何搭建属于你自己的提示词流水线。2. 核心需求解析我们为何需要超越字符串拼接在深入技术细节前我们必须先厘清一个根本问题为什么简单的字符串拼接在LLM应用开发中会迅速变得捉襟见肘这源于LLM应用本身复杂度的几个内在维度。2.1 动态性与上下文依赖一个实用的LLM应用其提示词极少是静态的。它通常依赖多种动态输入用户输入最直接的变量。检索到的上下文RAG场景从向量数据库查出的相关文档片段每次查询都不同。对话历史在多轮对话中需要将历史消息以特定格式如Human: ...\nAssistant: ...嵌入当前提示。工具/函数调用结果Agent场景Agent执行工具后需要将结果格式化并反馈给LLM进行下一步推理。系统变量如当前日期、用户名、会话ID等。用字符串拼接处理这些代码会迅速退化为难以阅读和维护的“面条代码”。你需要小心翼翼地处理换行符、缩进和JSON格式一个引号错误就可能导致模型输出完全偏离预期。2.2 可维护性与团队协作当提示词逻辑变得复杂时比如需要根据不同场景简单问答、深度分析、代码生成切换不同的指令模板或者需要为同一功能提供中英文版本。如果这些逻辑散落在各个业务函数中任何修改都将是灾难。你需要一个中心化的方式来管理这些模板确保一致性。此外清晰的模块化划分也让团队协作更顺畅前端工程师、算法工程师和产品经理可以更清晰地定义接口。2.3 可测试性与迭代优化提示词工程本身是一个需要不断实验和迭代的过程。你需要能方便地A/B测试不同版本的提示词观察哪个能带来更高的回答准确率或更低的Token消耗。当提示词是硬编码的字符串时替换和对比测试非常笨拙。而模板化之后你可以将模板视为可配置的“参数”轻松进行批量测试和效果评估。2.4 结构化输出与后处理现代LLM应用往往不满足于获取一段自由文本。我们可能需要模型输出结构化的数据比如一个JSON对象包含answer、confidence和citations字段。字符串拼接很难优雅地引导模型输出固定格式并对输出进行解析和验证。这需要将提示词模板与输出解析器Output Parser组合使用形成一个完整的“输入-处理-输出”流水线。基于以上痛点LangChain的提示词模板和组合流水线模式正是为了解决这些工程化问题而生的。它不是“语法糖”而是构建生产级LLM应用的必备基础设施。3. LangChain提示词模板基础与实战理解了“为什么”我们来看“怎么做”。首先从最基础的PromptTemplate开始。很多人以为它只是个简单的格式化工具其实不然它的设计蕴含了组合性的基因。3.1 基础PromptTemplate告别f-string最基本的用法是定义一个带有占位符的模板字符串。from langchain_core.prompts import PromptTemplate # 定义一个简单的模板 template “你是一个专业的翻译助手。请将以下英文翻译成中文\n\n英文{input_text}” prompt_template PromptTemplate.from_template(template) # 格式化生成最终提示词 formatted_prompt prompt_template.format(input_text“Hello, world!”) print(formatted_prompt)输出你是一个专业的翻译助手。请将以下英文翻译成中文 英文Hello, world!这看起来和f-string (f“你是一个...{input_text}”) 效果一样。但关键区别在于PromptTemplate是一个对象。它可以被序列化、存储、传递并且能与LangChain的其他组件如LLM、Chain无缝集成。这是构建流水线的第一步——将提示词“组件化”。3.2 多变量与部分格式化实际场景中变量往往不止一个。模板可以轻松处理多个变量。qa_template “””基于以下上下文回答问题。如果你不知道答案就说不知道不要编造。 上下文 {context} 问题{question} 答案””” prompt PromptTemplate.from_template(qa_template) # 假设我们从数据库获取了context retrieved_context “LangChain是一个用于开发由语言模型驱动的应用程序的框架...” user_question “LangChain是什么” final_prompt prompt.format(contextretrieved_context, questionuser_question)更强大的功能是部分格式化Partial。有些变量你可能在流水线的早期阶段就已知而另一些则需要稍后填充。例如系统指令可能一开始就固定了但用户问题需要运行时获取。from langchain_core.prompts import PromptTemplate # 定义一个包含多个变量的模板 template “系统指令{system_message}\n用户输入{user_input}” prompt PromptTemplate.from_template(template) # 部分格式化先提供system_message partial_prompt prompt.partial(system_message“你是一个乐于助人的助手。”) # 此时partial_prompt是一个新的模板它只剩下user_input一个待填变量 # 在后续流程中再填入user_input final_prompt partial_prompt.format(user_input“今天天气怎么样”)这个特性在构建链Chain时极其有用它允许你预先配置好链的部分参数。3.3 模板的序列化与持久化对于需要团队共享或频繁迭代的提示词将其存储在代码之外是更好的实践。LangChain支持将PromptTemplate保存为JSON或YAML文件。# 保存模板 prompt.save(“awesome_prompt.json”) # 在其他地方加载模板 loaded_prompt PromptTemplate.load(“awesome_prompt.json”)你可以将这些JSON文件纳入版本控制如Git方便追踪每次提示词优化的变更历史实现真正的“提示词即代码Prompt as Code”。注意PromptTemplate默认使用Jinja2语法这意味着你可以在模板中使用一些简单的控制逻辑如if-else判断但这会增加复杂度通常建议将复杂逻辑放在Python代码中保持模板的简洁和可读性。4. 构建组合流水线从模板到工作流单个模板解决了基础格式化问题但真正的威力在于组合。LangChain的核心抽象——链Chain就是用来将多个组件包括提示词模板、LLM、输出解析器等连接成流水线的工具。4.1 最简单的链LLMChainLLMChain是提示词模板与大语言模型的第一次组合。它抽象了“格式化提示词 - 调用LLM - 获取输出”这个过程。from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate # 更适用于聊天模型的模板 from langchain.chains import LLMChain # 1. 定义聊天提示模板ChatPromptTemplate更擅长处理消息角色 prompt ChatPromptTemplate.from_messages([ (“system”, “你是一个严谨的历史学家。”), (“human”, “请用{style}的风格简述{topic}的历史。”) ]) # 2. 初始化LLM llm ChatOpenAI(model“gpt-4”, temperature0.7) # 3. 创建链 chain LLMChain(llmllm, promptprompt) # 4. 运行链 result chain.invoke({“style”: “生动有趣”, “topic”: “罗马帝国”}) print(result[“text”]) # LLMChain的返回结果是一个字典包含输入、提示词和输出文本LLMChain将模板和模型绑定在一起你只需要关心输入变量。这已经比手动调用前进了一大步。4.2 引入输出解析器获得结构化结果很多时候我们需要模型输出JSON、列表等结构化数据而不是纯文本。OutputParser就派上用场了。我们可以创建一个“提示词模板 LLM 输出解析器”的流水线。from langchain_core.output_parsers import PydanticOutputParser from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.prompts import PromptTemplate # 1. 定义我们期望的数据结构 class BookSummary(BaseModel): title: str Field(description“书名”) author: str Field(description“作者”) summary: str Field(description“不超过100字的摘要”) genre: str Field(description“书籍类别”) # 2. 基于Pydantic模型创建解析器 parser PydanticOutputParser(pydantic_objectBookSummary) # 3. 构建提示词模板并自动注入输出格式指令 template “””请根据用户描述提取书籍信息。 {format_instructions} 用户描述{description} “”” # get_format_instructions()方法会生成一段告诉模型如何输出格式的文本 prompt PromptTemplate( templatetemplate, input_variables[“description”], partial_variables{“format_instructions”: parser.get_format_instructions()} ) # 4. 创建链这里我们手动串联实际可用LCEL from langchain_openai import ChatOpenAI llm ChatOpenAI(model“gpt-3.5-turbo”) # 链的逻辑prompt - llm - parser chain prompt | llm | parser # 使用LangChain表达式语言LCEL # 5. 运行 description “这是一本刘慈欣写的科幻小说讲的是地球人类文明和三体文明的信息交流、生死搏杀及两个文明在宇宙中的兴衰历程。” result chain.invoke({“description”: description}) print(result) # 输出BookSummary(title‘三体’ author‘刘慈欣’ summary‘...’ genre‘科幻小说’)这个流水线确保了输出永远是我们定义的BookSummary对象后续代码可以直接使用result.title、result.genre等属性极大地提升了下游处理的可靠性。4.3 使用LangChain表达式语言LCEL构建复杂流水线LCEL是LangChain中声明式组合组件的推荐方式它使用管道操作符|让流水线的构建像搭积木一样直观且强大。上面的例子已经初步展示了LCEL。让我们看一个更复杂的例子包含条件判断和分支。 假设我们有一个需求根据用户问题的复杂度决定是直接回答还是先联网搜索再回答。from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_community.tools import DuckDuckGoSearchRun from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableBranch, RunnableLambda # 定义组件 llm ChatOpenAI(model“gpt-3.5-turbo”) search DuckDuckGoSearchRun() output_parser StrOutputParser() # 子链1直接回答的链 direct_template ChatPromptTemplate.from_messages([ (“system”, “你是一个知识渊博的助手。”), (“human”, “{question}”) ]) direct_chain direct_template | llm | output_parser # 子链2先搜索再回答的链 search_template ChatPromptTemplate.from_messages([ (“system”, “你是一个助手。请根据以下搜索结果为用户提供答案。如果搜索结果不相关或不足请基于你自己的知识回答。\n\n搜索结果{search_results}”), (“human”, “问题{question}”) ]) def get_search_results(question: str) - dict: “”“执行搜索并返回结果。”“” search_result search.run(question) return {“search_results”: search_result, “question”: question} # 组合获取搜索 - 格式化提示词 - 调用LLM - 解析输出 search_chain RunnableLambda(get_search_results) | search_template | llm | output_parser # 路由链判断问题是否需要搜索 def route_question(state: dict): “”“根据问题内容决定路由。这是一个简单的启发式规则。”“” question state[“question”].lower() need_search_keywords [“最新” “新闻” “2024” “今天” “当前”] if any(keyword in question for keyword in need_search_keywords): return “search” else: return “direct” # 使用RunnableBranch构建分支流水线 branch RunnableBranch( (lambda x: route_question(x) “search”, search_chain), direct_chain ) # 主流水线输入问题 - 分支路由 - 执行对应链 full_chain {“question”: lambda x: x[“question”]} | branch # 运行 result1 full_chain.invoke({“question”: “勾股定理是什么”}) print(f“直接回答{result1}”) result2 full_chain.invoke({“question”: “今天国际油价最新行情是多少”}) print(f“搜索后回答{result2}”)这个例子展示了LCEL的强大之处通过RunnableBranch和RunnableLambda我们可以构建有判断、有分支、有组合的复杂工作流并且每个步骤都清晰可辨。这才是真正的“组合流水线”。5. 高级模式与最佳实践掌握了基础组合后我们来看一些能进一步提升效率和质量的高级模式和实战心得。5.1 少量示例Few-Shot模板对于需要引导模型输出特定格式或风格的任务在提示词中提供几个示例Few-Shot非常有效。FewShotPromptTemplate可以优雅地管理这些示例。from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate # 1. 定义示例集合 examples [ { “input”: “这部电影的视觉效果很棒但剧情太拖沓。”, “output”: “正面视觉效果。负面剧情拖沓。” }, { “input”: “电池续航惊人系统流畅就是屏幕有点小。”, “output”: “正面电池续航系统流畅。负面屏幕小。” } ] # 2. 定义单个示例的格式化模板 example_prompt PromptTemplate( input_variables[“input”, “output”], template“输入{input}\n输出{output}” ) # 3. 创建FewShotPromptTemplate few_shot_prompt FewShotPromptTemplate( examplesexamples, example_promptexample_prompt, prefix“你的任务是将用户评论总结为正面和负面要点。请参考以下示例”, suffix“输入{user_input}\n输出”, input_variables[“user_input”], example_separator“\n\n” # 示例之间的分隔符 ) # 4. 使用 formatted_prompt few_shot_prompt.format(user_input“餐厅环境优雅服务热情但菜品偏咸。”) print(formatted_prompt)这种方式将示例数据与提示词逻辑分离方便你通过增删示例来调整模型行为而无需改动核心代码。5.2 动态示例选择器当示例库很大时并非所有示例都相关。ExampleSelector可以根据输入动态选择最相关的几个示例提升效果并节省Token。from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate from langchain_core.example_selectors import SemanticSimilarityExampleSelector from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # ... 假设我们有大量的examples ... # 基于语义相似度选择示例 example_selector SemanticSimilarityExampleSelector.from_examples( examples, OpenAIEmbeddings(), Chroma, k2 # 每次选择2个最相关的示例 ) dynamic_prompt FewShotPromptTemplate( example_selectorexample_selector, # 使用选择器而非固定示例列表 example_promptexample_prompt, prefix“...”, suffix“...”, input_variables[“user_input”] ) # 对于不同的user_input动态生成的提示词中将包含不同的示例。5.3 提示词管理外部化与版本控制对于生产环境强烈建议将提示词模板外部化。你可以使用配置文件YAML/JSON将模板字符串和变量定义放在配置文件中。使用数据库对于需要动态更新、多租户或A/B测试的场景将模板存储在数据库中是更灵活的选择。结合LangChain Hub实验性LangChain社区提供了一个共享提示词的平台你可以从中查找和加载他人分享的优质模板。一个简单的YAML配置示例prompts/config.yamltranslation_prompt: template: | 你是一个专业的{domain}翻译。请将以下{source_lang}文本翻译成{target_lang}并保持专业术语准确。 原文{text} 翻译 input_variables: [“domain”, “source_lang”, “target_lang”, “text”] default_values: domain: “通用”然后在代码中加载import yaml with open(“prompts/config.yaml”, ‘r’) as f: prompt_configs yaml.safe_load(f) template_str prompt_configs[‘translation_prompt’][‘template’] # ... 创建PromptTemplate ...5.4 性能优化与调试Token计数与成本控制在调用LLM前使用get_num_tokens方法估算提示词的Token消耗对于长上下文管理至关重要。from langchain_openai import ChatOpenAI llm ChatOpenAI() num_tokens llm.get_num_tokens(formatted_prompt) print(f“提示词大约消耗 {num_tokens} tokens。”)结构化日志与追踪使用LangSmithLangChain官方平台或自定义日志记录每次链调用时的输入、输出和中间步骤尤其是格式化后的提示词。这是调试复杂流水线不可或缺的。缓存对于频繁出现且结果不变的子步骤如某些固定的信息提取可以考虑引入缓存机制如LangChain的RunnableWithFallbacks或外部缓存如Redis来降低成本和延迟。6. 常见陷阱与排查指南在实际构建流水线的过程中我踩过不少坑。这里总结几个最常见的问题和解决方法。6.1 变量不匹配错误问题运行chain.invoke时抛出KeyError提示某个变量在模板中未找到。原因提供给format或invoke的输入字典的键与模板中定义的input_variables不匹配。排查打印出你的PromptTemplate对象检查input_variables属性。确保你传入的字典包含了所有必需的变量。对于使用partial的模板确认哪些变量已被填充哪些仍需提供。注意变量名拼写和大小写必须完全一致。6.2 提示词格式化后格式混乱问题模型输出不符合预期可能是提示词中的换行、缩进在格式化后被破坏了。原因在模板字符串中使用三重引号时Python会保留所有缩进。如果模板定义在函数内函数体的缩进会混入模板。解决使用textwrap.dedent或在模板字符串起始处使用反斜杠\来消除不必要的缩进。from textwrap import dedent template dedent(“”” 你是一个助手。 请回答以下问题 {question} “””) # dedent会移除每行开头共同的空白符或者template ( “你是一个助手。\n” “请回答以下问题\n” “{question}” )6.3 LCEL链调试困难问题一个复杂的LCEL流水线报错难以定位是哪个环节出了问题。排查使用.stream()或.invoke()中间结果将长链拆分成小段分别测试每段的输入输出。利用with_config添加元数据为链的某个步骤添加标签方便在LangSmith等追踪工具中识别。from langchain_core.runnables import RunnableConfig debug_chain some_component.with_config(run_name“MyDebugStep”)捕获和检查中间状态对于RunnableLambda确保函数有清晰的日志输出或能处理异常。6.4 输出解析失败问题OutputParser抛出OutputParserException提示无法解析LLM的输出。原因LLM没有严格按照format_instructions中的要求输出。可能是指令不够清晰或任务对模型来说太复杂。解决强化指令在提示词中更明确地强调输出格式甚至使用“你必须输出一个合法的JSON字符串”等强约束语句。提供更清晰的示例在Few-Shot示例中展示完美的输出格式。使用更强大的模型对于复杂的结构化输出任务GPT-4通常比GPT-3.5表现更稳定。使用容错性更强的解析器如JsonOutputParser尝试解析JSON配合OutputFixingParser后者可以尝试让另一个LLM来修复格式错误的输出。from langchain.output_parsers import OutputFixingParser from langchain_openai import ChatOpenAI fixing_parser OutputFixingParser.from_llm(parseroriginal_parser, llmChatOpenAI())6.5 性能瓶颈问题流水线运行缓慢。排查分析各步骤耗时使用追踪工具或简单计时找出是哪个组件LLM调用、检索、复杂计算最耗时。优化提示词长度冗长的提示词会增加Token消耗和LLM响应时间。定期审查模板移除不必要的指令和示例。并行化如果流水线中有多个独立的步骤例如同时调用多个不同的工具可以考虑使用RunnableParallel来并行执行。缓存如前所述对确定性高的步骤引入缓存。从字符串拼接到组合流水线不仅仅是技术的升级更是开发范式的转变。它迫使你将LLM应用视为由一个个可测试、可复用、可监控的组件构成的数据流而非一堆胶水代码。我个人最大的体会是一旦习惯了这种思维开发复杂AI应用的信心和效率会成倍增长。开始可能会觉得LCEL或Runnable抽象有些绕但一旦上手你就会发现它带来的清晰度和可维护性是原始拼接方式无法比拟的。不妨从将一个现有的字符串拼接提示词改写成PromptTemplate开始再逐步尝试将其与一个LLM和简单的Parser组合成链你会立刻感受到其中的差别。
返回列表