各位同学,大家好!
今天,我们将深入探讨一个在大型语言模型(LLM)开发生命周期中至关重要且极具挑战性的话题:如何通过在线交互式操作,高效、持续地收集用户反馈,并将其反哺到我们的LangSmith标注数据集中,从而形成一个强大的数据飞轮,不断提升LLM的性能和鲁棒性。
在LLM的快速发展浪潮中,我们常常面临一个核心难题:如何有效地评估和改进模型。传统的离线评估固然重要,但它们往往无法完全捕捉到真实世界中用户与模型交互的复杂性和细微差别。用户在实际使用场景中的反馈,无论是直接的满意度评价、对输出的编辑修正,还是对特定行为的标注,都蕴含着极其宝贵的信息。而LangSmith,作为LangChain生态系统中的核心工具,为我们提供了追踪、评估和监控LLM应用的能力。将这两者结合起来,便能构建一个强大的闭环系统。
一、 引言:为什么在线反馈至关重要?
在深入技术细节之前,我们首先要理解为什么在线反馈在LLM开发中扮演着不可或缺的角色。
1.1 LLM开发的挑战与数据质量
大型语言模型(LLM)的开发是一个高度迭代的过程,涉及模型的选择、提示工程、检索增强生成(RAG)的构建、微调等多个环节。无论我们多么精心设计提示或微调模型,模型在面对真实世界的千变万化时,总会暴露出其局限性,例如:
- 幻觉(Hallucinations):模型生成看似合理但实际错误或虚构的信息。
- 不相关或冗余的输出:模型未能准确理解用户意图,生成了偏离主题或过于啰嗦的回答。
- 语调或风格不匹配:输出的语气、风格不符合用户预期或品牌调性。
- 安全与偏见问题:模型生成了不安全、有偏见或冒犯性的内容。
- 缺乏最新知识:模型知识库的滞后性导致无法回答时事问题。
解决这些问题的根本在于高质量的数据。传统的标注数据集通常是静态的,由人工在特定场景下创建。虽然它们为模型训练提供了基石,但往往难以覆盖所有边缘情况和用户行为的细微差别。
1.2 LangSmith的角色
LangSmith是LangChain团队开发的一个平台,旨在帮助开发者:
- 追踪(Tracing):记录LLM应用中的每一个操作,包括输入、输出、中间步骤、调用链等,形成可追溯的“踪迹”(Traces)。
- 评估(Evaluation):对这些踪迹进行自动化或人工评估,衡量模型性能。
- 监控(Monitoring):实时监控LLM应用的表现,识别潜在问题。
- 数据集管理(Dataset Management):创建、管理和版本控制用于评估和训练的数据集。
LangSmith的强大之处在于它将LLM的运行过程透明化,并提供了结构化的方式来收集和组织评估数据。然而,LangSmith本身主要侧重于离线或半自动化的评估流程。如何将散落在各个用户界面中的实时、动态的用户反馈,无缝地融入LangSmith的数据集,是我们需要解决的核心问题。
1.3 在线反馈的价值主张
在线反馈填补了离线评估的空白,带来了独特的价值:
- 真实世界的数据:直接从用户与应用的交互中获取,反映了最真实的使用场景和痛点。
- 多样性与覆盖度:用户群体和使用场景的广泛性,能够发现离线测试难以触及的边缘案例。
- 及时性:问题一旦出现,用户可以立即反馈,有助于我们快速定位和修复。
- 高关联性:反馈直接与特定的LLM输出和输入相关联,便于追溯和分析。
- 持续改进:形成一个数据飞轮,用户反馈驱动数据集更新,数据集驱动模型改进,模型改进带来更好的用户体验,从而吸引更多反馈。
通过将在线反馈高效地整合到LangSmith中,我们可以构建一个动态、自适应的LLM开发生态系统。
二、 LangSmith核心概念回顾
在着手设计和实现反馈系统之前,让我们快速回顾LangSmith的一些核心概念,它们将是我们集成工作的基石。
2.1 LangSmith数据模型
LangSmith的核心数据模型包括:
- Runs (运行):LangSmith中最小的可追踪单元。每一次LLM调用、工具使用、链的执行,都会产生一个Run。Run包含了输入、输出、状态、持续时间、元数据等信息。Run之间可以形成父子关系,构建完整的调用链。
- Traces (踪迹):一个完整的用户请求到LLM应用响应的端到端路径,通常由一系列相互关联的Runs组成,构成一个树状结构。
- Projects (项目):用于组织相关的Runs和Traces。通常一个LLM应用对应一个项目。
- Datasets (数据集):用于评估和微调的输入-输出对集合。数据集中的每个条目称为一个
Example,包含inputs和可选的outputs(作为参考输出或地面真值)。 - Feedback (反馈):附加到特定Run上的用户评价或标注。Feedback可以包含分数、评论、标签,甚至是一个修正后的输出。
2.2 LangSmith Client API
LangSmith提供了Python客户端库,允许我们以编程方式与平台进行交互。这是我们后端服务与LangSmith集成的关键。
import os
from langsmith import Client
from langsmith import traceable
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 确保环境变量已设置
# os.environ["LANGCHAIN_TRACING_V2"] = "true"
# os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
# os.environ["LANGCHAIN_API_KEY"] = "YOUR_LANGSMITH_API_KEY"
# os.environ["LANGCHAIN_PROJECT"] = "my-online-feedback-project" # 为你的项目命名
# 初始化LangSmith客户端
client = Client()
# 示例:一个简单的LangChain应用
model = ChatOpenAI(temperature=0)
prompt = ChatPromptTemplate.from_template("告诉我一个关于{topic}的冷知识。")
parser = StrOutputParser()
chain = prompt | model | parser
# 假设我们在一个Web应用中调用这个链
# @traceable(name="UserQueryChain", project_name="my-online-feedback-project")
def run_llm_query(topic: str):
"""模拟在应用中执行LLM查询并返回结果"""
# 在实际应用中,这里会通过LangChain的Runnable.invoke()或.stream()方法调用
# 并且如果 LANGCHAIN_TRACING_V2 开启,会自动创建Run和Trace
# 为了演示,我们手动模拟一个Run ID,因为在线反馈需要关联到具体的Run
# 实际场景中,我们会从 LangChain 的回调机制中获取 run_id
# 这里为了演示方便,我们假设已经获取了一个 run_id
# run_id = "..." # 假设这是一个实际的LangSmith Run ID
# 模拟 LangChain 链的执行
response = chain.invoke({"topic": topic})
# 在实际的生产环境中,当 LangChain 链执行时,如果 LangSmith 追踪开启,
# 那么会有一个 `run_id` 被生成并可以在回调中获取。
# 对于 Web 应用,通常会在请求处理的生命周期中捕获这个 run_id,
# 然后在返回给前端时,将这个 run_id 也一同返回,以便前端在用户提供反馈时能够回传。
# 为了本讲座的独立性,我们暂时不深入 LangChain 回调机制的细节,
# 而是假设前端能够通过某种方式(例如从后端 API 响应中)获取到当前 LLM 调用的 `run_id`。
# 模拟返回一个 run_id 和 LLM 输出
# 这是一个简化,实际的 run_id 是 UUID 对象
# from uuid import uuid4
# return {"output": response, "run_id": str(uuid4())} # 演示用途
# 更准确的做法是:当 LangChain 链执行时,它的 `config` 参数可以包含 `callbacks`,
# 其中可以指定 LangSmith 回调,并从回调中提取 `run_id`。
# 例如:
# from langchain_core.callbacks import LangChainTracer
# tracer = LangChainTracer(project_name="my-online-feedback-project")
# config = {"callbacks": [tracer]}
# response = chain.invoke({"topic": topic}, config=config)
# run_id = tracer.langsmith_run_id # 获取根run的ID
# return {"output": response, "run_id": str(run_id)}
# 为了简化,我们假设前端已经通过某种方式获取到了一个合法的 LangSmith run_id
# 这个run_id是LLM应用返回给用户的,用于标识本次交互
# 在后面的例子中,我们假设前端会传递这个 run_id 给后端。
return {"output": response, "run_id": "placeholder_run_id"} # 真实场景会是实际的 UUID
# 创建反馈的API
# client.create_feedback(
# run_id="<your-run-id>",
# key="user_satisfaction", # 反馈的键,如 "user_satisfaction", "accuracy", "relevance"
# score=1.0, # 分数,如1.0表示满意,0.0表示不满意
# comment="This was a great answer!", # 用户评论
# feedback_source_type="API" # 表明反馈来源是API
# )
# 创建或更新数据集中的Example
# client.create_example(
# dataset_id="<your-dataset-id>",
# inputs={"question": "如何煮鸡蛋?"},
# outputs={"answer": "将鸡蛋放入沸水中煮7-10分钟。"}
# )
# client.update_example(
# example_id="<your-example-id>",
# outputs={"answer": "将鸡蛋放入沸水中煮7分钟,然后放入冰水中冷却。"} # 修正后的输出
# )
三、 核心问题:弥合离线与在线的鸿沟
现有LangSmith的工作流,虽然能够追踪和评估,但在将实时用户反馈无缝地融入到其数据集更新流程方面,仍然存在一些挑战。
3.1 离线评估的局限性
- 滞后性:离线评估数据集通常是静态的,需要人工定期更新和标注,无法实时反映模型在生产环境中的表现变化。
- 覆盖不足:人工创建的数据集难以穷尽所有可能的输入、用户意图和环境因素。
- 成本高昂:大规模人工标注是耗时且昂贵的。
3.2 “人在回路”(Human-in-the-Loop)的必要性
为了克服这些局限性,我们需要将“人”引入到LLM的持续改进循环中。用户是应用最直接的体验者,他们的反馈是无价的。通过交互动作收集反馈,意味着:
- 点赞/点踩(Thumbs Up/Down):最直接的满意度指标。
- 编辑输出(Edit Output):用户直接纠正模型生成的内容,提供高质量的“地面真值”或“参考输出”。
- 分类/标签(Categorization/Tagging):用户对输出进行分类,如“相关”、“不相关”、“有害”、“有益”等。
- 打分(Rating):用户提供更细粒度的满意度分数。
- 评论(Comments):用户提供自由文本的额外上下文信息。
这些交互动作生成的反馈,需要一个结构化的方式来捕获、存储,并最终集成到LangSmith的追踪和数据集管理中。
四、 架构设计:构建在线反馈系统
一个完整的在线反馈收集系统,通常由前端、后端API和LangSmith集成层构成。
4.1 整体架构概览
以下是系统各组件及其交互的简化视图:
| 组件 | 职责 | 技术栈示例 |
|---|---|---|
| 前端 (Web UI) | 呈现LLM输出;提供用户反馈界面;向后端发送反馈数据。 | React/Vue/Angular, HTML/CSS |
| 后端 API | 接收前端反馈请求;验证数据;与LangSmith API交互;可选地存储原始反馈。 | Python (FastAPI/Flask), Node.js (Express) |
| LangSmith | 存储Runs/Traces;接收并存储Feedback;管理Datasets。 | LangSmith API |
| 数据存储 (可选) | 存储原始用户反馈,用于审计、重试和进一步分析。 | PostgreSQL, MongoDB, Redis |
数据流示意:
- 用户请求:用户在Web UI中输入查询。
- LLM调用:Web UI向后端发送查询,后端调用LLM(通过LangChain等框架)。
- LangSmith追踪:LLM调用被LangSmith追踪,生成
Run和Trace,并返回run_id。 - 结果展示:后端将LLM输出和
run_id返回给前端,前端展示输出。 - 用户反馈:用户对LLM输出进行交互(点赞、编辑等),前端收集反馈数据和
run_id。 - 反馈提交:前端将反馈数据发送到后端API的反馈端点。
- 后端处理:后端接收反馈,使用
run_id关联到LangSmith的相应Run,并通过LangSmith Client API创建Feedback。 - 数据集更新(可选):如果反馈是高质量的修正(如编辑后的输出),后端可能进一步利用此信息更新或创建LangSmith数据集中的
Example。
4.2 前端设计考量
前端是用户与反馈系统的直接接口,其设计应注重易用性。
- LLM输出展示区:清晰展示模型的原始输出。
- 反馈控制区:
- 快速反馈按钮:如点赞、点踩(👍👎),提供最便捷的反馈方式。
- 文本输入框:供用户输入详细评论或建议。
- 编辑/修正功能:允许用户直接修改模型输出,并提交修正后的版本。
- 分类/标签选择器:预设一些标签供用户选择。
- 会话上下文:确保每次反馈都能关联到具体的LLM调用 (
run_id)。通常,后端在返回LLM输出时,会一并返回本次调用的run_id,前端将其作为隐藏字段或状态管理起来。
4.3 后端API设计考量
后端API是连接前端和LangSmith的桥梁。
- API端点:设计清晰的RESTful或GraphQL端点来接收不同类型的反馈。例如:
/api/feedback,/api/correction。 - 数据模型:定义严格的数据模型来规范接收的反馈数据。这应包括
run_id、反馈类型、分数、评论、修正后的输出等。 - 数据验证:对接收到的数据进行严格验证,防止恶意输入或格式错误。
- LangSmith集成:使用LangSmith Client API与LangSmith平台进行交互。
- 异步处理:对于高并发场景,可以考虑将LangSmith的API调用放入后台任务队列,避免阻塞主线程。
- 错误处理与日志:记录所有失败的反馈提交,并提供适当的错误响应。
4.4 LangSmith集成策略
这是整个系统的核心。我们需要根据反馈类型,选择合适的LangSmith API进行交互。
主要API方法:
client.create_feedback():用于将用户反馈附加到特定的LangSmithRun上。这是最常用的方法。client.create_example():当用户提供了全新的高质量输入-输出对时(例如,用户主动提交了一个问题和他们认为完美的答案),可以创建新的数据集Example。client.update_example():如果用户对现有数据集中的某个Example提供了更正后的outputs,可以考虑更新该Example。但这通常需要更复杂的逻辑来将运行时Run与特定的Example关联起来。client.read_run():在需要获取某个Run的详细信息(如其inputs、outputs或所属的Dataset)时使用。
五、 实现细节:代码示例与逻辑
接下来,我们将通过具体的代码示例,演示如何实现一个在线反馈系统。我们将使用Python FastAPI作为后端框架,并结合LangSmith Python客户端。前端部分将使用React-like的伪代码进行概念性说明。
5.1 前端实现(概念性伪代码)
假设我们有一个Web页面,显示LLM的回复,并允许用户点赞、点踩或编辑。
// React-like 伪代码
import React, { useState, useEffect } from 'react';
function LLMResponseDisplay({ initialQuery }) {
const [query, setQuery] = useState(initialQuery);
const [llmOutput, setLlmOutput] = useState('');
const [currentRunId, setCurrentRunId] = useState(null);
const [feedbackComment, setFeedbackComment] = useState('');
const [editedOutput, setEditedOutput] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 1. 模拟向后端发送LLM查询并获取结果
const fetchLlmResponse = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/llm-query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic: query })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setLlmOutput(data.output);
setCurrentRunId(data.run_id); // 从后端获取 LangSmith run_id
setEditedOutput(data.output); // 初始化编辑内容为LLM原始输出
} catch (e) {
setError('Failed to fetch LLM response: ' + e.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (query) {
fetchLlmResponse();
}
}, [query]);
// 2. 提交反馈到后端
const submitFeedback = async (feedbackType, score = null) => {
if (!currentRunId) {
alert('无法提交反馈,未找到关联的LLM运行ID。');
return;
}
const payload = {
run_id: currentRunId,
feedback_type: feedbackType, // e.g., "thumbs_up", "thumbs_down", "comment", "correction"
comment: feedbackComment,
score: score,
correction_output: isEditing && feedbackType === 'correction' ? editedOutput : null,
};
try {
const response = await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
alert('反馈提交成功!' + data.message);
// 清除反馈状态
setFeedbackComment('');
setIsEditing(false);
} catch (e) {
setError('提交反馈失败: ' + e.message);
}
};
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: 'auto' }}>
<h1>LLM 交互界面</h1>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="输入你的问题..."
style={{ width: '100%', padding: '10px', marginBottom: '10px' }}
/>
<button onClick={fetchLlmResponse} disabled={loading} style={{ padding: '10px 15px' }}>
{loading ? '加载中...' : '获取LLM回复'}
</button>
{error && <p style={{ color: 'red' }}>错误: {error}</p>}
{llmOutput && (
<div style={{ border: '1px solid #ccc', padding: '15px', marginTop: '20px', borderRadius: '5px' }}>
<h2>LLM 回复:</h2>
{isEditing ? (
<textarea
value={editedOutput}
onChange={(e) => setEditedOutput(e.target.value)}
style={{ width: '100%', minHeight: '150px', marginBottom: '10px' }}
/>
) : (
<p>{llmOutput}</p>
)}
<div style={{ display: 'flex', gap: '10px', marginTop: '15px' }}>
<button onClick={() => submitFeedback('thumbs_up', 1.0)}>👍 满意</button>
<button onClick={() => submitFeedback('thumbs_down', 0.0)}>👎 不满意</button>
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? '取消编辑' : '编辑并提交修正'}
</button>
{isEditing && (
<button onClick={() => submitFeedback('correction', 1.0)}>提交修正</button>
)}
</div>
<div style={{ marginTop: '20px' }}>
<textarea
value={feedbackComment}
onChange={(e) => setFeedbackComment(e.target.value)}
placeholder="输入您的评论(可选)"
style={{ width: '100%', minHeight: '80px', marginBottom: '10px' }}
/>
<button onClick={() => submitFeedback('comment')}>提交评论</button>
</div>
{currentRunId && <p style={{ fontSize: '0.8em', color: '#666' }}>LangSmith Run ID: {currentRunId}</p>}
</div>
)}
</div>
);
}
export default LLMResponseDisplay;
前端逻辑摘要:
- 用户输入查询,点击按钮,前端调用后端
/api/llm-query接口。 - 后端返回LLM的
output以及本次调用的run_id。 - 前端展示
output,并将run_id存储在组件状态中。 - 用户通过点赞、点踩、编辑或评论等方式提供反馈。
- 前端将
run_id和反馈数据一同发送到后端/api/feedback接口。
5.2 后端实现 (Python FastAPI)
后端将承担处理LLM请求、与LangSmith交互以及接收并处理用户反馈的核心职责。
import os
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from uuid import UUID, uuid4
from typing import Optional, Dict, Any
from langsmith import Client, traceable
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.callbacks import LangChainTracer
# --- LangSmith 配置 ---
# 确保环境变量已设置
# os.environ["LANGCHAIN_TRACING_V2"] = "true"
# os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
# os.environ["LANGCHAIN_API_KEY"] = "YOUR_LANGSMITH_API_KEY"
# os.environ["LANGCHAIN_PROJECT"] = "online-feedback-collection-demo" # 你的项目名称
# 检查 LangSmith 环境变量是否设置
if not all(os.getenv(var) for var in ["LANGCHAIN_ENDPOINT", "LANGCHAIN_API_KEY", "LANGCHAIN_PROJECT"]):
print("WARNING: LangSmith environment variables not fully set. Tracing might not work.")
print("Please set LANGCHAIN_ENDPOINT, LANGCHAIN_API_KEY, LANGCHAIN_PROJECT.")
# For demonstration, we'll set dummy values if not present
os.environ.setdefault("LANGCHAIN_ENDPOINT", "https://api.smith.langchain.com")
os.environ.setdefault("LANGCHAIN_API_KEY", str(uuid4())) # Dummy key
os.environ.setdefault("LANGCHAIN_PROJECT", "online-feedback-collection-demo")
os.environ.setdefault("LANGCHAIN_TRACING_V2", "true")
client = Client()
app = FastAPI()
# --- LangChain LLM 链定义 ---
model = ChatOpenAI(temperature=0, model="gpt-3.5-turbo") # 使用一个具体的模型
prompt = ChatPromptTemplate.from_template("告诉我一个关于{topic}的冷知识。")
parser = StrOutputParser()
llm_chain = prompt | model | parser
# --- Pydantic 模型定义 ---
class LLMQueryPayload(BaseModel):
topic: str
class FeedbackPayload(BaseModel):
run_id: UUID # 必须关联到 LangSmith 的一个 Run
feedback_type: str # 例如: "thumbs_up", "thumbs_down", "comment", "correction"
score: Optional[float] = None # 例如: 1.0 (满意), 0.0 (不满意)
comment: Optional[str] = None
correction_output: Optional[str] = None # 如果是修正类型反馈,提供修正后的输出
# 更多自定义字段可以通过 `extra_data` 传递
extra_data: Optional[Dict[str, Any]] = None
# --- API 端点 ---
@app.post("/api/llm-query")
async def handle_llm_query(payload: LLMQueryPayload):
"""
接收用户查询,调用LLM,并将LLM输出和LangSmith Run ID返回给前端。
"""
tracer = LangChainTracer(project_name=os.environ["LANGCHAIN_PROJECT"])
config = {"callbacks": [tracer]}
try:
# 调用LLM链,LangSmith会通过tracer自动追踪
response = llm_chain.invoke({"topic": payload.topic}, config=config)
# 获取根 Run 的 ID
root_run_id = tracer.langsmith_run_id
if not root_run_id:
raise HTTPException(status_code=500, detail="Failed to get LangSmith Run ID.")
print(f"Generated LangSmith Run ID: {root_run_id}")
return {"output": response, "run_id": root_run_id}
except Exception as e:
print(f"Error during LLM query: {e}")
raise HTTPException(status_code=500, detail=f"Error processing LLM query: {str(e)}")
@app.post("/api/feedback")
async def post_user_feedback(payload: FeedbackPayload):
"""
接收用户反馈,并将其提交到LangSmith。
如果反馈包含修正后的输出,则尝试更新或创建数据集Example。
"""
print(f"Received feedback for run_id: {payload.run_id}, type: {payload.feedback_type}")
try:
# 1. 创建 LangSmith Feedback
# 构造 feedback_kwargs
feedback_kwargs = {
"run_id": str(payload.run_id),
"key": payload.feedback_type,
"score": payload.score,
"comment": payload.comment,
"feedback_source_type": "API", # 标记反馈来源
}
# 对于修正类型的反馈,可以将修正后的内容作为 'value' 存储
if payload.correction_output:
feedback_kwargs["value"] = payload.correction_output
# 也可以在这里添加一个额外的 key,例如 "corrected_output_value"
# client.create_feedback(run_id=str(payload.run_id), key="corrected_output_value", value=payload.correction_output)
if payload.extra_data:
feedback_kwargs["extra_data"] = payload.extra_data
langsmith_feedback = client.create_feedback(**feedback_kwargs)
print(f"Created LangSmith feedback with ID: {langsmith_feedback.id}")
# 2. (可选) 根据反馈类型进一步处理,例如更新或创建数据集Example
# 这是一个更复杂的场景,需要确定该 Run 是否关联到某个 Example,以及哪个数据集。
# 这里我们演示一种简单的策略:如果用户提供了 "correction" 类型的反馈,
# 并且包含了 `correction_output`,我们可以考虑创建一个新的 Example
# 或者更新一个现有 Example(如果能找到对应的原始 Example)。
if payload.feedback_type == "correction" and payload.correction_output:
# 尝试获取原始 Run 的信息,以便获取其输入
try:
original_run = client.read_run(payload.run_id)
if original_run and original_run.inputs:
# 获取 Run 的输入,作为新 Example 的输入
# 注意:这里假设 Run 的 inputs 是一个字典,且直接可用作 Example 的 inputs
example_inputs = original_run.inputs
# 假设我们有一个预设的 LangSmith 数据集 ID
# 在实际应用中,这个 dataset_id 应该通过配置或者从原始 Run 的元数据中获取
# 比如,如果你的LLM应用是基于某个数据集进行测试的,那么这个 Run 可能已经关联了 dataset_id 和 example_id
# 为了演示,我们先创建一个新的数据集,或者使用一个已有的
# 如果你还没有数据集,可以手动在 LangSmith UI 创建,或者通过 client.create_dataset()
dataset_name = os.environ["LANGCHAIN_PROJECT"] + "-corrected-examples"
try:
dataset = client.read_dataset(dataset_name=dataset_name)
except Exception: # Dataset not found, create a new one
print(f"Dataset '{dataset_name}' not found, creating new dataset.")
dataset = client.create_dataset(dataset_name=dataset_name,
description="User corrected examples for LLM improvement.")
# 创建一个新的 Example,将用户修正后的输出作为参考输出
# 这样,这个新的 Example 就可以用于未来的评估或模型微调
new_example = client.create_example(
dataset_id=dataset.id,
inputs=example_inputs,
outputs={"text": payload.correction_output}, # 假设输出是文本格式
metadata={"source_run_id": str(payload.run_id), "feedback_id": str(langsmith_feedback.id)}
)
print(f"Created new LangSmith Example with ID: {new_example.id} from user correction.")
return {"message": "Feedback and corrected example submitted successfully", "feedback_id": str(langsmith_feedback.id), "example_id": str(new_example.id)}
else:
print(f"Could not retrieve inputs for run_id {payload.run_id} to create example.")
except Exception as e:
print(f"Error reading original run or creating example: {e}")
# 即使创建 Example 失败,Feedback 仍然是成功的
pass
return {"message": "Feedback submitted successfully", "feedback_id": str(langsmith_feedback.id)}
except Exception as e:
print(f"Error submitting feedback: {e}")
raise HTTPException(status_code=500, detail=f"Failed to submit feedback: {str(e)}")
后端逻辑摘要:
-
handle_llm_query端点:- 接收前端的用户查询。
- 创建一个
LangChainTracer实例,将其作为回调传递给LLM链。 - 执行LLM链。
LangChainTracer会自动捕获本次LLM调用的所有Run信息到LangSmith。 - 从
tracer中获取根run_id。 - 将LLM的
output和run_id返回给前端。
-
post_user_feedback端点:- 接收前端发送的
FeedbackPayload,其中包含run_id和用户反馈的具体内容。 - 使用
client.create_feedback()方法,将用户的反馈(如点赞、点踩、评论等)附加到LangSmith中对应的run_id上。key字段用于标识反馈的类型(例如thumbs_up、user_comment、correction),score和comment字段存储具体评分和文本。 - 高级处理 – 数据集更新/创建:
- 如果
feedback_type是correction且提供了correction_output,这表明用户提供了一个高质量的修正。 - 我们首先尝试使用
client.read_run(payload.run_id)获取原始LLM调用时的inputs。 - 然后,我们查找或创建一个专门用于存储用户修正示例的LangSmith数据集。
- 最后,使用
client.create_example()方法,将原始inputs和用户修正后的correction_output作为一个新的Example添加到该数据集中。这样,这些用户修正的数据就可以被用于未来的模型评估或微调。
- 如果
- 接收前端发送的
5.3 运行演示
要运行上述代码,你需要:
- 安装必要的Python库:
pip install fastapi uvicorn langsmith langchain-openai pydantic - 设置LangSmith环境变量。
- 保存后端代码为
main.py。 - 运行FastAPI应用:
uvicorn main:app --reload - 在浏览器中打开一个简单的HTML/JavaScript页面,或者使用Postman/Insomnia来测试API。
一个简单的HTML/JS前端测试页面 (minimal.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM Feedback Demo</title>
<style>
body { font-family: sans-serif; margin: 20px; }
.container { max-width: 800px; margin: auto; border: 1px solid #eee; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
input[type="text"], textarea { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; }
button:hover { background-color: #0056b3; }
button:disabled { background-color: #cccccc; cursor: not-allowed; }
.feedback-section { margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px; }
.llm-output { background-color: #f9f9f9; padding: 15px; border-radius: 4px; margin-top: 15px; white-space: pre-wrap; word-wrap: break-word; }
.error-message { color: red; margin-top: 10px; }
</style>
</head>
<body>
<div class="container">
<h1>LLM 交互式反馈演示</h1>
<div>
<label for="queryInput">你的问题:</label>
<input type="text" id="queryInput" placeholder="请输入你的问题,例如:关于猫的冷知识">
<button id="submitQuery">获取LLM回复</button>
<div id="loadingIndicator" style="display: none;">加载中...</div>
<div id="errorMessage" class="error-message"></div>
</div>
<div id="llmResponseContainer" style="display: none;">
<h2>LLM 回复:</h2>
<div id="llmOutput" class="llm-output"></div>
<p style="font-size: 0.8em; color: #666;">LangSmith Run ID: <span id="runIdDisplay"></span></p>
<div class="feedback-section">
<h3>提供反馈:</h3>
<button id="thumbsUp">👍 满意</button>
<button id="thumbsDown">👎 不满意</button>
<button id="toggleEdit">编辑并提交修正</button>
<div id="editSection" style="display: none; margin-top: 15px;">
<textarea id="editedOutput" rows="5"></textarea>
<button id="submitCorrection">提交修正</button>
</div>
<div style="margin-top: 15px;">
<textarea id="commentInput" placeholder="输入您的评论(可选)" rows="3"></textarea>
<button id="submitComment">提交评论</button>
</div>
</div>
</div>
</div>
<script>
const queryInput = document.getElementById('queryInput');
const submitQueryBtn = document.getElementById('submitQuery');
const loadingIndicator = document.getElementById('loadingIndicator');
const errorMessage = document.getElementById('errorMessage');
const llmResponseContainer = document.getElementById('llmResponseContainer');
const llmOutputDiv = document.getElementById('llmOutput');
const runIdDisplay = document.getElementById('runIdDisplay');
const thumbsUpBtn = document.getElementById('thumbsUp');
const thumbsDownBtn = document.getElementById('thumbsDown');
const toggleEditBtn = document.getElementById('toggleEdit');
const editSection = document.getElementById('editSection');
const editedOutputTextarea = document.getElementById('editedOutput');
const submitCorrectionBtn = document.getElementById('submitCorrection');
const commentInput = document.getElementById('commentInput');
const submitCommentBtn = document.getElementById('submitComment');
let currentRunId = null;
let originalLlmOutput = '';
let isEditing = false;
async function fetchLlmResponse() {
const query = queryInput.value.trim();
if (!query) {
alert('请输入问题!');
return;
}
loadingIndicator.style.display = 'block';
errorMessage.textContent = '';
llmResponseContainer.style.display = 'none';
currentRunId = null;
originalLlmOutput = '';
isEditing = false;
editSection.style.display = 'none';
try {
const response = await fetch('/api/llm-query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic: query })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
originalLlmOutput = data.output;
llmOutputDiv.textContent = originalLlmOutput;
currentRunId = data.run_id;
runIdDisplay.textContent = currentRunId;
editedOutputTextarea.value = originalLlmOutput; // Initialize edited output
llmResponseContainer.style.display = 'block';
} catch (e) {
errorMessage.textContent = '获取LLM回复失败: ' + e.message;
} finally {
loadingIndicator.style.display = 'none';
}
}
async function submitFeedback(feedbackType, score = null, comment = null, correctionOutput = null) {
if (!currentRunId) {
alert('无法提交反馈,未找到关联的LLM运行ID。');
return;
}
const payload = {
run_id: currentRunId,
feedback_type: feedbackType,
score: score,
comment: comment || commentInput.value,
correction_output: correctionOutput,
};
try {
const response = await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
alert('反馈提交成功!' + data.message);
commentInput.value = ''; // Clear comment
if (feedbackType === 'correction') {
isEditing = false;
editSection.style.display = 'none';
toggleEditBtn.textContent = '编辑并提交修正';
llmOutputDiv.textContent = originalLlmOutput; // Revert display to original after correction
}
} catch (e) {
errorMessage.textContent = '提交反馈失败: ' + e.message;
}
}
submitQueryBtn.addEventListener('click', fetchLlmResponse);
thumbsUpBtn.addEventListener('click', () => submitFeedback('thumbs_up', 1.0));
thumbsDownBtn.addEventListener('click', () => submitFeedback('thumbs_down', 0.0));
submitCommentBtn.addEventListener('click', () => submitFeedback('comment'));
toggleEditBtn.addEventListener('click', () => {
isEditing = !isEditing;
if (isEditing) {
editSection.style.display = 'block';
llmOutputDiv.textContent = editedOutputTextarea.value; // Show editable area
toggleEditBtn.textContent = '取消编辑';
} else {
editSection.style.display = 'none';
llmOutputDiv.textContent = originalLlmOutput; // Revert to original LLM output
toggleEditBtn.textContent = '编辑并提交修正';
}
});
submitCorrectionBtn.addEventListener('click', () => {
const correctedText = editedOutputTextarea.value.trim();
if (!correctedText) {
alert('修正内容不能为空!');
return;
}
if (correctedText === originalLlmOutput) {
alert('修正内容与原内容相同,无需提交。');
return;
}
submitFeedback('correction', 1.0, '用户修正', correctedText);
});
editedOutputTextarea.addEventListener('input', () => {
// While editing, update the display div to reflect changes
if (isEditing) {
llmOutputDiv.textContent = editedOutputTextarea.value;
}
});
</script>
</body>
</html>
将上述HTML保存为 index.html,然后你可以直接在浏览器中打开它,并与运行中的FastAPI后端进行交互。
5.4 关键点与注意事项
run_id的传递:这是连接前端反馈和LangSmith中特定LLM运行的唯一标识。确保后端在每次LLM调用后将其返回给前端,前端在提交反馈时将其带回。- 反馈类型标准化:定义清晰的
feedback_type(如thumbs_up,thumbs_down,user_comment,correction)有助于后续分析和自动化处理。 - 数据集管理:对于
correction类型的反馈,我们选择创建一个新的Example。在更复杂的场景中,你可能需要一套策略来:- 识别该
Run是否来源于一个已有的Example。 - 如果来源于
Example,是更新该Example的reference_output,还是创建一个新的版本? - 将修正后的数据归入哪个数据集?是模型的通用训练集,还是专门的“问题案例”集?
- 识别该
- 错误处理与幂等性:API调用可能失败,考虑重试机制。同时,确保重复提交相同的反馈不会导致数据异常。
- 安全性:验证所有用户输入,防止XSS、SQL注入等攻击。对敏感数据进行加密。
- 异步处理:LangSmith API调用可能需要一些时间。对于高并发应用,考虑使用Celery等任务队列异步处理反馈提交,避免阻塞Web服务器。
六、 数据流与管理
6.1 完整数据流
-
用户浏览器:
- 发送
/api/llm-query(POST,{topic: "..."}) - 接收
{output: "...", run_id: "..."} - 用户交互,触发
submitFeedback - 发送
/api/feedback(POST,{run_id: "...", feedback_type: "...", ...})
- 发送
-
后端 FastAPI 应用:
/api/llm-query:- 调用 LangChain LLM 链
- LangChain Tracers 将
Run和Trace发送到 LangSmith - 返回
output和run_id
/api/feedback:- 接收
FeedbackPayload - 调用
langsmith.Client.create_feedback()将反馈关联到run_id - 如果反馈是
correction且包含correction_output:- 调用
langsmith.Client.read_run()获取原始inputs - 调用
langsmith.Client.create_dataset()(如果不存在) - 调用
langsmith.Client.create_example()将inputs和correction_output作为新 Example 添加到数据集
- 调用
- 返回处理结果
- 接收
-
LangSmith 平台:
- 存储
Run和Trace信息 - 存储附加到
Run上的Feedback - 管理
Dataset和Example
- 存储
6.2 数据库考量(可选但推荐)
虽然 LangSmith 负责存储 LLM 运行和反馈,但在后端引入一个自己的数据库(如 PostgreSQL)仍然非常有益:
- 审计日志:存储所有用户反馈的原始记录,作为审计追踪和合规性的基础。
- 重试机制:如果 LangSmith API 调用失败,可以将反馈数据存储在本地数据库中,并实现一个后台任务进行重试,确保数据不丢失。
- 额外元数据:存储 LangSmith 不直接支持的、与业务逻辑更相关的反馈元数据。
- 统计分析:可以对本地存储的反馈数据进行更灵活的自定义统计和可视化,而无需频繁查询 LangSmith API。
数据库表示例 (PostgreSQL):
CREATE TABLE user_feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
run_id UUID NOT NULL,
feedback_type VARCHAR(50) NOT NULL,
score DECIMAL(3, 2), -- 例如 0.0 到 1.0
comment TEXT,
correction_output TEXT,
user_id UUID, -- 如果有用户系统,关联用户ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
langsmith_feedback_id UUID, -- 存储 LangSmith 返回的反馈ID
langsmith_example_id UUID, -- 如果创建了Example,存储其ID
status VARCHAR(20) DEFAULT 'pending' -- 例如 'pending', 'processed', 'failed'
);
-- 索引可以加速查询
CREATE INDEX idx_user_feedback_run_id ON user_feedback (run_id);
CREATE INDEX idx_user_feedback_created_at ON user_feedback (created_at);
通过将反馈数据首先写入本地数据库,然后异步地将其同步到 LangSmith,可以提高系统的鲁棒性和可扩展性。
七、 实践考量与最佳实践
7.1 用户体验
- 反馈入口显眼且便捷:让用户容易找到并快速提交反馈。
- 多种反馈方式:提供快速点赞/点踩,也提供详细文本输入和编辑功能。
- 清晰的提示:告知用户反馈的作用,鼓励他们提供有价值的信息。
- 即时反馈:提交成功后给予用户确认,提升参与感。
7.2 隐私与安全
- 数据脱敏:如果用户反馈可能包含敏感信息,考虑在存储前进行脱敏处理。
- 访问控制:确保只有授权的服务可以访问和修改反馈数据。
- 合规性:遵守GDPR、CCPA等数据隐私法规。
7.3 可扩展性
- 异步处理:对于高流量应用,将LangSmith API调用放入消息队列(如Kafka, RabbitMQ)进行异步处理,避免后端API成为瓶颈。
- 批量提交:LangSmith API目前没有直接的批量提交反馈接口,但可以在后端批量处理并逐个提交,或者通过自定义的LangSmith集成逻辑实现。
- 监控:监控反馈系统的健康状况,包括API响应时间、错误率、反馈提交量等。
7.4 迭代改进
- 定期分析反馈:不仅要收集,更要分析反馈数据,识别常见问题模式。
- 反馈驱动的提示工程:用户评论和修正可以直接用于改进LLM提示词。
- 高质量数据用于微调:用户修正后的
Example是模型微调的宝贵数据源。 - 定制评估指标:基于用户反馈,可以开发更贴合业务需求的自定义评估指标。
- A/B测试:利用反馈数据来比较不同模型版本或提示策略的效果。
7.5 伦理考量
- 偏见:用户反馈本身可能带有偏见。在利用反馈数据进行模型改进时,需要警惕并采取措施减轻潜在的偏见放大效应。
- 代表性:确保收集到的反馈具有代表性,避免过度优化特定用户群体的需求而损害其他用户体验。
- 透明度:在可能的情况下,向用户解释他们的反馈如何被用于改进系统。
八、 将反馈融入LLM开发生命周期
在线反馈不仅仅是数据点,更是LLM持续改进的引擎。
- Prompt Engineering优化:
- 通过分析“不满意”的反馈及相关评论,识别提示词的不足之处。
- 利用用户提供的“修正输出”,提炼出更好的指令或Few-shot示例,直接优化提示词。
- RAG系统改进:
- 如果反馈指出LLM输出的信息不准确或缺失,可以检查RAG检索到的文档是否相关或全面。
- 用户对输出的修正可能暗示我们需要调整检索策略或更新知识库。
- 模型微调数据:
- 通过在线反馈收集到的高质量“输入-用户修正输出”对,可以直接作为SFT(Supervised Fine-Tuning)的训练数据。
- 尤其是用户对幻觉、不当输出的纠正,对于提升模型安全性、准确性至关重要。
- 自动化评估与回归测试:
- 将用户修正后的Example添加到LangSmith数据集中,可以用于自动化评估,确保模型在修复现有问题时不会引入新的回归。
- 这些Example可以作为“黄金标准”测试用例。
- A/B测试与灰度发布:
- 通过A/B测试不同版本的LLM应用或提示,比较用户反馈指标(如满意度、修正率),选择表现更好的版本进行推广。
- 将部分用户流量引导至新版本,通过在线反馈收集数据,进行小范围的灰度发布。
九、 总结与展望
通过本讲座,我们深入探讨了如何构建一个将在线用户反馈反哺至LangSmith标注数据集的系统。我们从理解在线反馈的价值出发,回顾了LangSmith的核心概念,设计了前端、后端和LangSmith集成的架构,并提供了详细的代码示例。这个系统使得LLM的开发不再是静态的、线性的过程,而是一个动态的、以用户为中心的闭环迭代。
持续收集、分析和利用用户反馈,是构建真正智能、健壮和受用户喜爱的LLM应用的关键。未来,我们可以进一步探索更智能化的反馈分析工具,自动化从反馈中提取知识并生成新的训练数据,甚至实现反馈驱动的自适应模型调整。这将是LLM领域持续演进的重要方向。