三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从手拼Prompt到工程化:构建可维护的企业级AI助手Prompt层

从手拼Prompt到工程化:构建可维护的企业级AI助手Prompt层

1. 项目缘起:从“手工作坊”到“工程化产线”

如果你正在或计划在企业内部落地一个基于大语言模型的问答助手,尤其是那种需要对接公司内部文档、产品手册、规章制度等非公开数据的“知识库助手”,那么下面这个场景你一定不陌生:为了回答不同部门、不同业务线的问题,你写了几十个甚至上百个独立的 Prompt。销售部问产品卖点,你写一个prompt_sales.md;技术支持问故障排查,你写一个prompt_support.md;新员工问公司制度,你又写一个prompt_hr.md。一开始还能应付,但随着业务变化、知识库更新、模型升级,维护这些散落各处的 Prompt 就成了噩梦。改一个通用的系统指令(System Prompt),你得在所有文件里手动查找替换;新增一个工具调用(Function Calling),你得确保每个相关 Prompt 都同步更新;更别提为了提升回答质量,不断进行的 A/B 测试和微调了。这就像用记事本和复制粘贴来管理一个大型软件项目的源代码,效率低下且极易出错。

这正是“手拼 Prompt”时代的典型困境:缺乏结构、难以复用、维护成本高昂。而本项目的核心目标,就是带领大家跳出这个“手工作坊”,搭建一个可维护、可扩展、工程化的 Prompt 层。这不是简单地介绍几个 Prompt 模板,而是构建一套从设计、开发、测试到部署的完整体系。我们将以构建一个“企业知识库助手”为实战背景,深入探讨如何利用 RAG、Agent 等架构思想,并基于 LangChain 这类流行框架,将零散的 Prompt 转化为结构化的、可配置的、甚至可版本控制的“工程资产”。无论你是刚开始接触 AI 应用的开发者,还是正在为现有 AI 项目技术债发愁的团队负责人,这套方法都能为你提供一条清晰的演进路径。

2. 核心理念:什么是“可维护的 Prompt 层”?

在深入代码之前,我们必须先统一思想:什么是“可维护的 Prompt 层”?它不是一个具体的库或工具,而是一种设计和组织 Prompt 的方法论。其核心在于将 Prompt 从“一段文本”提升为“一个具有清晰输入、输出、逻辑和依赖关系的组件”。

2.1 从“文本”到“组件”的思维转变

传统 Prompt 是一段扁平的文本,混合了指令、上下文、示例和输出格式要求。而组件化思维要求我们对其进行解构:

  1. 系统角色与约束:定义 AI 助手的固定人设、行为边界和通用规则。这部分相对稳定,是所有对话的基石。
  2. 上下文注入:根据用户问题,动态地从知识库、数据库或会话历史中检索并插入相关信息。这部分是动态的,是 RAG 的核心。
  3. 工具/函数描述:定义 AI 可以调用的外部能力,如查询数据库、调用 API、执行计算等。这部分需要清晰的结构化描述,以便模型理解。
  4. 思维链与输出格式:引导模型推理过程,并严格规定其返回数据的结构(如 JSON)。这部分确保了输出的机器可读性和稳定性。
  5. 少量示例:提供一两个典型范例,帮助模型快速掌握任务模式。

一个“可维护的 Prompt 层”会将这些部分模块化,通过配置或代码逻辑进行组装,而非硬编码在一个字符串里。

2.2 关键特征:可维护性体现在哪?

  • 模块化:如上所述,各部分分离,可以独立修改和测试。例如,更新知识库检索策略无需改动系统指令。
  • 可配置化:通过配置文件(如 YAML、JSON)或环境变量来控制 Prompt 的行为,例如切换严谨模式或创意模式,调整检索文档的数量。
  • 版本控制:Prompt 组件应该像代码一样,能用 Git 进行版本管理,方便回溯、对比和协作。
  • 可测试性:能够针对特定的 Prompt 组件或组装后的完整 Prompt 进行单元测试和集成测试,验证其输出是否符合预期。
  • 中心化管理:避免 Prompt 散落在各个业务代码中,而是集中在一个或几个特定的目录或服务里进行管理。

2.3 与 RAG、Agent 架构的关系

  • RAG:是“上下文注入”模块的核心技术。一个良好的 Prompt 层需要与 RAG 流水线(文档加载、切分、向量化、检索)优雅集成。Prompt 层负责定义“如何利用检索到的上下文”,例如,指令可以是:“请严格依据以下背景资料回答问题,如果资料中未提及,请明确告知‘根据现有资料无法回答’。”
  • Agent:是 Prompt 层的“执行引擎”。Agent 负责理解用户意图、管理多轮对话、决定何时以及如何调用“工具/函数描述”中定义的能力。Prompt 层为 Agent 提供了推理和决策的“蓝图”。例如,System Prompt 中会写明:“你是一个助手,可以调用搜索工具来获取最新信息。在回答关于实时数据的问题前,请先尝试调用搜索工具。”

理解了这些理念,我们就知道,搭建 Prompt 层不仅仅是字符串拼接,而是设计一个微型的、专为与大模型交互而生的“领域特定语言”框架。

3. 技术选型与基础环境搭建

在实战中,我们选择LangChain作为核心框架。它虽然不是唯一选择,但其丰富的组件、活跃的社区以及对 Prompt 模板、RAG、Agent 的原生支持,使其成为实现我们目标的优秀起点。请注意,我们的重点是方法论,理解了 LangChain 的设计,你也能轻松迁移到 LlamaIndex、Dify 或其他自研框架上。

3.1 为什么是 LangChain?

  • Prompt 模板:原生支持ChatPromptTemplateFewShotPromptTemplate等,允许我们轻松创建模块化的 Prompt。
  • LCEL:LangChain 表达式语言,让我们能用链式(pipe)的方式组合组件,代码非常声明式和直观。
  • 丰富的集成:支持众多向量数据库、大模型、工具等,减少造轮子的工作。
  • Agent 抽象:提供了清晰的 Agent 执行循环、工具调用等抽象,是我们构建智能助手的基础。

注意:坊间常有 LangChain “抽象泄露”、“性能开销”的批评。在简单场景下直接调用模型 API 确实更轻量。但当我们面临复杂的、需要组合多种组件、且对可维护性有高要求的企业场景时,LangChain 提供的结构和范式能显著降低长期成本。关键在于“正确使用”,而非“盲目使用”。

3.2 项目初始化与核心依赖

我们从一个干净的 Python 环境开始。建议使用uvpoetry进行依赖管理,这里以pip示例。

# 创建项目目录并进入 mkdir enterprise-knowledge-assistant && cd enterprise-knowledge-assistant python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate # 安装核心依赖 pip install langchain langchain-community langchain-openai # 安装用于RAG的向量数据库客户端(以Chroma为例,轻量易用) pip install chromadb # 安装文档加载和处理工具 pip install pypdf python-dotenv tiktoken # 安装Web框架(用于构建简单API) pip install fastapi uvicorn

创建.env文件来管理敏感配置,如 API Keys:

OPENAI_API_KEY=your_openai_api_key_here # 后续可添加其他如数据库连接、向量库地址等

3.3 设计项目目录结构

一个清晰的目录结构是“可维护性”的物理体现。我推荐如下结构:

enterprise-knowledge-assistant/ ├── app/ │ ├── __init__.py │ ├── core/ # 核心逻辑层 │ │ ├── __init__.py │ │ ├── prompts/ # **Prompt层核心目录** │ │ │ ├── __init__.py │ │ │ ├── system.py # 系统指令定义 │ │ │ ├── templates.py # 各类Prompt模板 │ │ │ └── few_shot_examples.py # 少样本示例 │ │ ├── chains/ # 业务链定义 │ │ ├── agents/ # Agent定义 │ │ └── models.py # 数据模型(Pydantic) │ ├── knowledge_base/ # RAG知识库相关 │ │ ├── loader.py # 文档加载 │ │ ├── splitter.py # 文档切分 │ │ ├── vector_store.py # 向量库操作 │ │ └── retriever.py # 检索器封装 │ └── api/ # API层 │ └── endpoints.py ├── configs/ # 配置文件 │ └── settings.yaml ├── data/ # 原始文档数据 ├── tests/ # 测试 ├── .env ├── requirements.txt └── main.py # 应用入口

这个结构将“Prompt层”明确放在了app/core/prompts/下,使其成为一个独立的、受关注的模块。

4. 构建核心:模块化 Prompt 层实战

现在,让我们开始构建 Prompt 层的核心模块。我们将遵循从稳定到动态、从通用到特定的顺序。

4.1 定义系统指令

app/core/prompts/system.py中,我们定义不同场景下的系统角色。这些指令是助手行为的“宪法”,通常很稳定。

# app/core/prompts/system.py from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate class SystemPrompts: """系统指令定义""" @staticmethod def get_general_assistant() -> SystemMessagePromptTemplate: """通用助手角色""" system_template = """ 你是一个专业、准确且乐于助人的AI助手,服务于{company_name}公司。 你的核心职责是依据用户的问题和提供的上下文信息,提供清晰、准确的回答。 你必须遵守以下规则: 1. **诚实与准确**:如果提供的上下文信息不足以回答用户问题,你必须明确告知“根据现有资料,我无法回答这个问题”,并可以建议用户提供更多信息或联系相关同事。 2. **安全与合规**:不得生成任何有害、歧视性、违法或违反公司政策的内容。 3. **聚焦与简洁**:回答应紧扣问题,避免无关的展开。除非用户要求,否则优先提供要点。 4. **格式与清晰**:对于复杂信息,合理使用列表、表格或分点阐述以提高可读性。 当前对话背景:{conversation_context} """ return SystemMessagePromptTemplate.from_template(system_template) @staticmethod def get_strict_qa() -> SystemMessagePromptTemplate: """严格问答模式,用于知识库精确查询""" system_template = """ 你是一个严格的信息验证助手。你的任务**仅限**于根据提供的“参考上下文”来回答问题。 你的回答必须满足: 1. **严格引用**:答案中的每一个关键事实都必须能在“参考上下文”中找到明确依据。 2. **禁止臆测**:严禁基于外部知识或内部推理进行补充、延伸或猜测。 3. **处理未知**:如果“参考上下文”中完全没有相关信息,你的回答必须是:“根据所提供的资料,该问题暂无明确答案。” 4. **指明出处**:如果可能,在答案末尾以括号形式注明该信息来源于哪份文档(例如:来源于《2024年产品白皮书》)。 参考上下文: {context} """ return SystemMessagePromptTemplate.from_template(system_template)

实操心得:系统指令不宜过长或过于复杂,否则模型可能无法完全遵循。将不同职责(如通用对话、严格检索、创意写作)拆分成不同的系统指令模板,通过配置切换,比一个庞大的、充满条件判断的指令更有效。

4.2 创建可复用的 Prompt 模板

app/core/prompts/templates.py中,我们创建组装完整 Prompt 的模板。这里我们将使用 LangChain 的ChatPromptTemplate,它支持组合多个MessagePromptTemplate

# app/core/prompts/templates.py from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate from .system import SystemPrompts class PromptTemplates: """可复用的完整Prompt模板""" @staticmethod def get_rag_qa_prompt(company_name: str = "我们公司") -> ChatPromptTemplate: """用于知识库问答的标准RAG Prompt模板""" # 1. 获取系统指令 system_message = SystemPrompts.get_strict_qa() # 2. 定义人类问题模板 human_template = "用户问题:{question}" human_message = HumanMessagePromptTemplate.from_template(human_template) # 3. 组装成ChatPromptTemplate # 注意:`ChatPromptTemplate.from_messages` 接受的顺序就是消息在对话中的顺序 chat_prompt = ChatPromptTemplate.from_messages([ system_message, # 系统消息在前 human_message # 用户消息在后 ]) # 4. 部分格式化(Partial):有些变量我们可能想提前绑定,比如公司名 # 但这里company_name在strict_qa模板里没用,我们演示一个通用模板的例子 # 对于strict_qa,关键的输入变量是 `context` 和 `question` return chat_prompt @staticmethod def get_agent_conversational_prompt() -> ChatPromptTemplate: """用于支持工具调用的Agent对话Prompt模板""" from langchain.prompts import MessagesPlaceholder # Agent通常需要系统指令、聊天历史、用户输入和Agent暂存器(scratchpad) system_message = SystemPrompts.get_general_assistant() # MessagesPlaceholder 是一个占位符,允许我们在运行时动态插入消息列表 # 这对于管理多轮对话历史至关重要 chat_prompt = ChatPromptTemplate.from_messages([ system_message, MessagesPlaceholder(variable_name="chat_history"), # 对话历史 HumanMessagePromptTemplate.from_template("{input}"), # 当前用户输入 MessagesPlaceholder(variable_name="agent_scratchpad") # Agent思考过程 ]) return chat_prompt

关键点解析

  • ChatPromptTemplate.from_messages:这是构建对话式 Prompt 的核心。它定义了一个消息序列。
  • MessagesPlaceholder:这是实现“可维护性”的魔法组件。它允许我们将动态生成的内容(如多轮对话历史、Agent 的思考步骤)作为变量插入到固定的 Prompt 结构中,而不是通过字符串拼接。这保证了核心模板的干净和稳定。

4.3 实现上下文管理:集成 RAG

Prompt 层需要与 RAG 流水线对接,动态注入检索到的上下文。我们在app/core/chains/中创建一个链来实现这个逻辑。

# app/core/chains/rag_chain.py from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_openai import ChatOpenAI from app.core.prompts.templates import PromptTemplates from app.knowledge_base.retriever import get_retriever # 假设已实现 from langchain_core.runnables import RunnablePassthrough def create_rag_qa_chain(): """ 创建完整的RAG问答链。 流程:用户问题 -> 检索器 -> 注入上下文到Prompt -> 大模型生成答案。 """ # 1. 初始化模型 llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1) # 低temperature保证答案稳定 # 2. 获取Prompt模板 qa_prompt = PromptTemplates.get_rag_qa_prompt() # 3. 创建“文档组合链”:负责将检索到的文档和问题填入Prompt,并调用LLM # `create_stuff_documents_chain` 会将检索到的所有文档“塞进”Prompt中指定的 `context` 变量 combine_docs_chain = create_stuff_documents_chain(llm, qa_prompt) # 4. 获取检索器(这里需要你根据实际向量库实现) retriever = get_retriever() # 5. 创建“检索链”:将检索器和文档组合链连接起来 # `create_retrieval_chain` 是一个高阶函数,它自动处理: # a. 用用户问题调用检索器,得到相关文档。 # b. 将“问题”和“检索到的文档”一起传递给 `combine_docs_chain`。 retrieval_chain = create_retrieval_chain(retriever, combine_docs_chain) return retrieval_chain # 使用示例 if __name__ == "__main__": chain = create_rag_qa_chain() result = chain.invoke({"input": "公司今年的年假政策是怎样的?"}) print(result["answer"])

为什么这样设计?我们将 RAG 流程封装在一个链里,但 Prompt 模板 (qa_prompt) 是独立配置的。如果想从“严格模式”切换到“概括模式”,只需修改PromptTemplates.get_rag_qa_prompt()返回的模板即可,无需改动链的其他部分。这体现了“可维护性”——业务逻辑与 Prompt 表现层解耦。

5. 进阶:构建支持工具调用的 Agent

当问题超出知识库范围,需要查询实时数据、执行计算或调用内部 API 时,我们就需要 Agent。Agent 的本质是一个循环:理解问题 -> 决定行动(思考)-> 执行工具 -> 观察结果 -> 继续循环或给出最终答案。

5.1 定义工具

首先,在app/core/agents/tools.py中定义 Agent 可以使用的工具。工具本质上是一个函数,加上清晰的描述(这个描述就是给模型看的 Prompt)。

# app/core/agents/tools.py from langchain.tools import tool from datetime import datetime import requests @tool def search_company_news(keywords: str) -> str: """ 搜索公司内部新闻公告。 Args: keywords: 搜索关键词,如“年会”、“晋升”。 Returns: 返回与关键词相关的新闻摘要列表。 """ # 这里模拟一个内部API调用 # 在实际项目中,这里会是调用真正的内部新闻系统API print(f"[工具调用] 正在搜索公司新闻,关键词: {keywords}") # 模拟返回 return f"1. 2024年5月10日:公司举办年度技术创新大会。\n2. 2024年4月1日:新员工入职培训计划更新。\n(此为模拟数据)" @tool def calculate_annual_leave(join_date: str, working_years: int) -> str: """ 根据入职日期和司龄计算年假天数。 Args: join_date: 入职日期,格式 YYYY-MM-DD。 working_years: 员工司龄(整数)。 Returns: 计算出的年假天数及说明。 """ try: join = datetime.strptime(join_date, "%Y-%m-%d") base_days = 5 # 基础年假 additional_days = max(0, working_years - 1) # 每多一年加一天,最多10天 total_days = min(base_days + additional_days, 15) return f"根据政策,您的年假天数为 {total_days} 天(基础{base_days}天,司龄加成{additional_days}天)。" except ValueError: return "日期格式错误,请使用 YYYY-MM-DD 格式。"

5.2 创建 Agent 执行器

接下来,我们使用 LangChain 的 Agent 框架来绑定工具、Prompt 和模型。

# app/core/agents/assistant_agent.py from langchain.agents import create_openai_tools_agent, AgentExecutor from langchain_openai import ChatOpenAI from app.core.prompts.templates import PromptTemplates from .tools import search_company_news, calculate_annual_leave def create_knowledge_assistant_agent(): """ 创建企业知识库助手Agent。 该Agent可以回答问题,并在需要时调用工具。 """ # 1. 定义工具列表 tools = [search_company_news, calculate_annual_leave] # 2. 初始化LLM。对于工具调用,建议使用较新的模型,如 gpt-3.5-turbo 或 gpt-4 llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # 注意:为了稳定调用工具,temperature通常设为0或接近0 # 3. 获取Agent专用的Prompt模板 prompt = PromptTemplates.get_agent_conversational_prompt() # 4. 创建Agent # `create_openai_tools_agent` 会生成一个符合OpenAI Function Calling格式的Agent agent = create_openai_tools_agent(llm, tools, prompt) # 5. 创建Agent执行器,它封装了思考-行动-观察的循环逻辑 # `handle_parsing_errors=True` 非常重要!当模型输出不符合工具调用格式时,尝试自动修复。 # `max_iterations=5` 防止Agent陷入死循环。 agent_executor = AgentExecutor( agent=agent, tools=tools, verbose=True, # 打印详细执行过程,调试时非常有用 handle_parsing_errors=True, max_iterations=5, early_stopping_method="generate", # 当Agent认为该给出最终答案时停止 ) return agent_executor # 使用示例 if __name__ == "__main__": agent = create_knowledge_assistant_agent() # 注意:Agent的输入格式需要匹配Prompt中的变量名,这里`input`对应Prompt里的`{input}` # `chat_history` 和 `agent_scratchpad` 由AgentExecutor自动管理,我们通常只需传入`input` result = agent.invoke({ "input": "帮我查一下最近有没有关于技术大会的新闻?另外,我是2020-06-01入职的,现在有几年司龄了?", # 如果是多轮对话,还需要传入 `chat_history` # "chat_history": [...] }) print(result["output"])

关键点解析

  1. 工具描述即 Prompt@tool装饰器下的函数文档字符串(docstring)会被自动用作给模型看的工具描述。务必写得清晰、准确,说明输入、输出和功能,这直接决定了模型能否正确调用它。
  2. Prompt 的变量get_agent_conversational_prompt()返回的模板包含了chat_historyagent_scratchpad占位符。AgentExecutor会在运行时自动填充这些内容。我们主要关心input变量。
  3. 错误处理handle_parsing_errors=True是生产环境的必备选项。大模型有时会输出非标准 JSON,这个设置能让执行器尝试修复或让模型重试,避免整个对话崩溃。

6. 配置化与中心化管理

为了让 Prompt 层真正易于维护,我们需要将其与代码逻辑进一步分离,实现配置化。

6.1 使用 YAML 管理 Prompt 模板

configs/prompt_templates.yaml中定义模板:

# configs/prompt_templates.yaml system_prompts: general_assistant: | 你是一个专业、准确且乐于助人的AI助手,服务于{company_name}公司。 ... (同上,略) ... strict_qa: | 你是一个严格的信息验证助手... ... (同上,略) ... rag_templates: standard: | {system_prompt} 参考上下文: {context} 用户问题:{question} summarized: | {system_prompt} 请基于以下背景资料,用简洁的语言概括性回答用户问题。 背景资料:{context} 问题:{question} agent_instructions: base: | {system_prompt} 你可以使用以下工具: {tools} 在决定使用工具前,请先简要思考一下是否必要。 使用工具时,必须严格按照工具要求的格式提供参数。 如果不需要使用工具,请直接给出友好、专业的回答。

然后在代码中加载:

# app/core/prompts/manager.py import yaml import os from langchain.prompts import PromptTemplate, ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate class PromptManager: _prompts_config = None @classmethod def load_config(cls, config_path: str = "configs/prompt_templates.yaml"): with open(config_path, 'r', encoding='utf-8') as f: cls._prompts_config = yaml.safe_load(f) @classmethod def get_rag_template(cls, template_name: str = "standard") -> ChatPromptTemplate: if cls._prompts_config is None: cls.load_config() template_str = cls._prompts_config['rag_templates'][template_name] # 这里需要根据YAML中的结构,解析出system和user部分,并构建ChatPromptTemplate # 示例:假设YAML里是完整模板,我们可以用from_template prompt = ChatPromptTemplate.from_template(template_str) return prompt

6.2 动态切换 Prompt 策略

在实际应用中,我们可能根据用户身份、问题类型或业务场景动态选择不同的 Prompt。这可以在链的层面通过RunnableBranch或条件逻辑实现。

# app/core/chains/router_chain.py from langchain_core.runnables import RunnableBranch, RunnableLambda from .rag_chain import create_rag_qa_chain from app.core.agents.assistant_agent import create_knowledge_assistant_agent def classify_question_type(input_dict: dict) -> str: """一个简单的分类器,判断问题类型""" question = input_dict.get("question", "").lower() if any(word in question for word in ["新闻", "公告", "计算", "查询"]): return "need_agent" else: return "pure_qa" def create_router_chain(): """路由链:根据问题类型,决定走纯RAG流程还是Agent流程""" rag_chain = create_rag_qa_chain() agent_chain = create_knowledge_assistant_agent() # 定义分支 branch = RunnableBranch( (lambda x: classify_question_type(x) == "need_agent", agent_chain), rag_chain # 默认分支 ) return branch # 使用:用户问题统一入口 router = create_router_chain() result = router.invoke({"question": "计算一下我的年假", "input": "计算一下我的年假"}) # Agent链需要input key

注意事项:这里的分类器classify_question_type非常简单。在生产环境中,你可能需要一个更精细的分类模型,或者利用大模型自身进行意图识别(这又是一个有趣的 Prompt 设计点)。关键是,这种架构将路由逻辑与具体的处理链解耦,使得增加新的问题类型(如“转人工客服”)变得非常容易。

7. 测试、监控与迭代

一个可维护的系统离不开完善的测试和监控。

7.1 对 Prompt 进行单元测试

Prompt 也是代码,需要测试。我们可以测试其格式是否正确、在给定输入下是否产生预期的输出结构(即使内容不完全一致)。

# tests/test_prompts.py import pytest from langchain_core.prompts import ChatPromptTemplate from app.core.prompts.manager import PromptManager def test_rag_prompt_format(): """测试RAG Prompt模板是否能正确格式化""" prompt = PromptManager.get_rag_template("standard") # 测试输入变量是否齐全 assert all(var in prompt.input_variables for var in ["system_prompt", "context", "question"]) # 测试格式化 formatted = prompt.format( system_prompt="你是一个助手。", context="这是背景。", question="这是一个问题吗?" ) assert "这是一个问题吗?" in formatted assert "这是背景。" in formatted def test_agent_prompt_includes_tools(): """测试Agent Prompt是否包含工具描述占位符""" from app.core.prompts.templates import PromptTemplates prompt = PromptTemplates.get_agent_conversational_prompt() # Agent Prompt 应该包含 agent_scratchpad 这个占位符 # 我们可以检查 input_variables 或 messages 的结构 # 这里简化检查 assert isinstance(prompt, ChatPromptTemplate)

7.2 构建评估流水线

对于核心的问答对,我们需要评估 Prompt 修改后效果是提升还是下降。

  1. 创建测试集:在data/eval/下存放qa_pairs.jsonl,每条记录包含question,reference_answer,context
  2. 编写评估脚本:使用 LLM 本身(如 GPT-4)作为裁判,或者结合精确匹配、相似度计算(如余弦相似度)进行自动评估。
  3. A/B 测试:在灰度发布时,将新旧 Prompt 分配给不同用户群,收集满意度评分或人工审核结果。
# scripts/evaluate_prompt.py import json from langchain.evaluation import load_evaluator from app.core.chains.rag_chain import create_rag_qa_chain def evaluate_on_dataset(prompt_version: str): chain = create_rag_qa_chain() # 这里可以传入不同的prompt版本参数 evaluator = load_evaluator("pairwise_string") # 示例:使用成对比较评估器 with open("data/eval/qa_pairs.jsonl", "r") as f: score = 0 for line in f: data = json.loads(line) prediction = chain.invoke({"input": data["question"]})["answer"] # 这里可以调用评估器,或者计算BLEU/ROUGE分数 # 简化:如果预测答案包含关键信息点则加分 if any(keyword in prediction for keyword in data["key_points"]): score += 1 accuracy = score / total_questions print(f"Prompt版本 {prompt_version} 在测试集上的准确率: {accuracy:.2%}")

7.3 监控与日志

AgentExecutor和关键链中启用verbose=True在开发时很有用。在生产环境,则需要结构化的日志。

  • 记录每次交互:记录用户问题、使用的 Prompt 模板/版本、检索到的文档 ID、调用的工具、模型回复、耗时等。这有助于事后分析和调试。
  • 监控异常:特别是工具调用失败、模型输出格式错误、检索结果为空等情况。
  • 收集反馈:在界面提供“回答是否有用”的反馈按钮,将反馈数据与当时的交互日志关联,用于优化 Prompt 和检索策略。

8. 常见问题与排查技巧实录

在实际搭建和运行过程中,你会遇到各种各样的问题。以下是我从多个项目中总结出的高频问题及解决方案。

8.1 模型不遵循指令或“胡言乱语”

  • 症状:模型忽略系统指令中的约束,或者开始编造知识库中没有的信息。
  • 排查与解决
    1. 检查指令清晰度:指令是否冗长矛盾?用更简短、强硬的语句,如“必须”、“禁止”。将最重要的规则放在最前面。
    2. 调整上下文位置:确保系统指令在 Prompt 的最开始。有些模型对消息顺序敏感。
    3. 使用更强大的模型gpt-3.5-turbo在复杂指令遵循上不如gpt-4系列。如果关键业务场景,考虑升级模型。
    4. 降低 Temperature:将temperature设为 0 或 0.1,减少随机性,使输出更可控。
    5. 添加强制分隔符:在上下文和问题之间使用如---###这样的明显分隔符,帮助模型区分。

8.2 RAG 效果差,检索不到相关文档或答案不准

  • 症状:答案与问题无关,或者“根据资料无法回答”的比例过高。
  • 排查与解决
    1. 文档切分(Chunking)策略:这是影响 RAG 效果的首要因素。不要简单按固定字符数切分。
      • 尝试递归切分:优先按段落、标题切分,再按句子或固定长度切分,保留语义完整性。
      • 增加重叠:在相邻 Chunk 之间保留 10-20% 的重叠文字,防止关键信息被切断。
    2. 检索器优化
      • 尝试混合搜索:结合向量相似度搜索(语义)和关键词搜索(如 BM25),取长补短。LangChain 的EnsembleRetriever可以做到。
      • 调整检索数量k值不是越大越好。从 4 开始测试,根据答案质量调整。太多无关文档会干扰模型。
      • 重排序:对检索到的 Top N 个结果,用小模型或交叉编码器进行二次排序,将最相关的排在前面。可以集成CohereBAAI/bge-reranker等重排模型。
    3. Prompt 优化:在 Prompt 中明确指令模型“只根据以下上下文回答”,并说明如何处理未知问题。可以加入少量示例。

8.3 Agent 频繁错误调用工具或陷入循环

  • 症状:Agent 在不该调用工具时调用,或反复调用同一个工具而不给出最终答案。
  • 排查与解决
    1. 优化工具描述:工具的函数名和文档字符串要极度清晰。在描述中明确使用场景和限制。例如:“此工具仅用于查询2024年之后的新闻”。
    2. 设置max_iterations:务必设置一个合理的上限(如 5-10),防止死循环。
    3. 使用handle_parsing_errors=True:这能避免因模型输出格式轻微错误导致的整个流程中断。
    4. 提供更丰富的上下文:在系统指令中,给 Agent 更明确的思考框架,例如:“首先,理解用户问题。其次,判断是否需要工具。如果需要,选择最合适的工具并准备好参数。最后,根据工具结果组织答案。”
    5. 考虑使用 ReAct 或 Plan-and-Execute 模式:LangChain 提供了不同的 Agent 类型。OPENAI_FUNCTIONS类型(我们用的)适合简单工具调用。对于复杂规划,可以尝试ZERO_SHOT_REACT_DESCRIPTION或使用LangGraph来构建有状态的、更可控的工作流。

8.4 性能与延迟问题

  • 症状:响应速度慢,尤其是第一次查询。
  • 排查与解决
    1. 向量索引优化:确保向量数据库的索引已构建。对于大规模知识库,考虑使用HNSW等近似搜索算法在精度和速度间取得平衡。
    2. 异步处理:对于文档加载、向量化等耗时操作,使用异步 IO。FastAPI 等框架支持异步端点。
    3. 缓存:对常见的、不变的问题答案进行缓存。甚至可以对语义相似的查询进行缓存(需要向量相似度匹配)。
    4. 模型选择:在保证效果的前提下,选择更快的模型。例如,用gpt-4o-mini替代gpt-4进行初步回答或重排序。
    5. 流式输出:对于长文本生成,使用模型的流式响应接口,让用户能边生成边看到部分结果,提升体验。

8.5 版本管理与回滚

  • 问题:修改了 Prompt 后线上效果变差,如何快速回滚?
  • 解决方案
    • Git 管理configs/prompt_templates.yamlapp/core/prompts/下的所有代码必须纳入 Git 版本控制。
    • 配置标识:每次发布新 Prompt,在配置中或通过环境变量设置一个版本号,如PROMPT_VERSION=v2.1
    • 功能开关:在代码中,可以通过判断版本号或功能开关,动态加载不同版本的 Prompt 模板。这样可以通过修改配置,瞬间切换回旧版本。
    • 数据库存储:对于更复杂的系统,可以将 Prompt 模板存储在数据库,并附带版本和发布时间,后台可灵活切换和灰度。

搭建一个可维护的 Prompt 层,初期会花费比“手拼 Prompt”更多的时间,但这是完全值得的。它带来的长期收益是巨大的:清晰的架构让团队协作成为可能;配置化管理让迭代和 A/B 测试变得轻松;模块化设计让复用和扩展成本降到最低。当你的企业知识库助手需要从回答 HR 问题扩展到支持销售、客服、研发等多个场景时,你会庆幸当初打下了这个坚实的基础。

← 返回列表