一个反馈按钮不能自动告诉你答案错在哪里
设想用户等了 18 秒,看到一段没有引用的回答,最后点击 👎。这个动作至少可能表达四件不同的事:答案事实错误、引用不可信、等待太久、页面没有解释过程。若后端只保存 liked=false,这些问题会被压成一个无法行动的数字。
反馈闭环的目标不是收集更多按钮,而是让每条反馈都能回到当时的运行。例如用户评价“引用不足”,开发者应能从 run_id 找到:检索节点返回了几个来源、最终回答引用了哪几个、使用了哪个模型和提示词版本,以及用户评价的是哪一条消息。本文使用“资料调研助手”作为贯穿案例,反馈只评价一次已经完成的回答,不把它伪装成训练标签。
先设计问题,再设计按钮
第一版可以使用二元反馈加可选原因:
有帮助 / 没帮助
没帮助原因:事实不准确|引用不足|过程太慢|格式不符合预期|其他
补充说明:可选,限制长度按钮旁边应明确反馈对象是“这条回答”还是“整次运行”。如果一个运行产生多条消息,反馈必须带 message_id;如果用户评价的是过程,则需要另一个 target=run,不能靠前端当前选中项猜测。
前端只提交事实和用户意图,不把当前页面的完整 State 上传:
type FeedbackRequest = {
client_event_id: string;
run_id: string;
message_id: string;
value: "positive" | "negative";
reason?: "incorrect" | "citation" | "slow" | "format" | "other";
comment?: string;
ui_version: string;
};client_event_id 由客户端为一次点击生成,用于重复提交幂等。run_id 和 message_id 必须由服务端校验归属;不能因为用户提交了一个字符串,就允许他评价或读取别人的运行。
后端先保存事实,再异步处理
反馈接口的同步职责很小:鉴权、校验运行归属、做幂等写入并返回结果。分类、脱敏、关联 Trace 和进入评估集可以异步执行,避免页面因为下游分析不可用而失败:
def submit_feedback(user, request: FeedbackRequest) -> FeedbackResponse:
run = run_repository.get(request.run_id)
if run is None or run.owner_id != user.id:
raise PermissionError("run is not accessible")
if not run.has_message(request.message_id):
raise ValueError("message does not belong to run")
row = feedback_repository.insert_idempotent(
client_event_id=request.client_event_id,
run_id=request.run_id,
message_id=request.message_id,
value=request.value,
reason=request.reason,
comment=redact(request.comment),
app_version=run.app_version,
model_version=run.model_version,
)
feedback_queue.publish({"feedback_id": row.id})
return FeedbackResponse(id=row.id, accepted=True)这段代码刻意没有把反馈直接写进模型提示词,也没有在请求里同步调用 Phoenix。数据库记录的是用户实际做过的事;异步任务再把 feedback_id 与 Trace、版本和任务类型关联起来。这样 Phoenix 暂时不可用时,反馈事实仍然不会丢,后续仍可补做关联。
反馈和 Phoenix Trace 如何关联
Phoenix1 提供 Trace 的查询和关联入口;反馈记录需要保存能定位到对应运行的标识。
反馈表至少要能关联:
| 字段 | 作用 |
|---|---|
run_id / message_id | 找到用户评价的具体运行和消息 |
trace_id | 在 Phoenix 打开对应 Trace |
thread_id | 理解恢复和多轮上下文 |
app_version / prompt_version / model_version | 比较版本变化 |
task_type / tenant_id | 按业务场景和权限分桶 |
created_at | 计算反馈延迟和灰度窗口 |
如果 Trace 中没有 run_id,可以在应用入口补上;如果反馈表没有 trace_id,也可以通过受控的运行映射表查询。不要让前端直接提交一个 Phoenix 查询 URL,更不要把完整 Trace 内容复制进反馈表。
关联成功后,开发者可以从一条负反馈看到完整的调查路径:检索是否为空、哪个节点重试、模型版本和 token、最终事件是否超时。Trace 解释“系统做了什么”,反馈说明“用户认为结果是否有用”;只有前者,团队可能优化一个用户根本不在意的问题,只有后者,又不知道应该改哪一步。
监控的不只是反馈比例
全局负反馈率很容易误导。一个版本可能减少了点击,却同时提高了等待时长;另一个版本可能让用户更满意,但成本翻倍。建议按任务类型、版本和原因同时看:
质量:正 / 负反馈率、引用覆盖、人工复核通过率
运行:p50 / p95 延迟、失败率、重试率、暂停时长
成本:每次运行 token、模型费用、工具调用次数
体验:从 run.started 到首个可见事件的时间、断线率指标标签使用有限枚举,例如 task_type 和 reason;不要把 run_id、用户评论或 prompt 作为高基数指标标签。原始反馈和 Trace 仍保存在受控存储中,指标只用于聚合趋势。
把样本变成可处理的问题桶
负反馈不是标准答案,它只是一个需要解释的信号。比如用户标记“事实错误”,仍要检查引用、检索时间和任务上下文,不能直接把用户评论当作正确答案。异步处理可以先按原因分桶,再由人工复核高影响样本:
查看 Mermaid 源码
flowchart LR
A[用户反馈] --> B[鉴权与幂等写入]
B --> C[脱敏与关联 Trace]
C --> D{问题分桶}
D -->|事实 / 引用| E[检查检索与证据]
D -->|过程慢| F[检查节点与模型耗时]
D -->|格式问题| G[检查输出协议与 UI]
D -->|其他| H[人工复核]分桶规则可以迭代,但原始用户选择和评论应保持不可变,避免改了分类后无法复盘历史。
从反馈沉淀评估集,而不是直接训练
经过脱敏和人工确认的样本才适合进入评估集。一个样本至少包含输入摘要、期望条件、实际输出、引用、运行版本和失败标签:
{
"case_id": "case-2026-001",
"input": {"topic": "LangGraph 持久化"},
"expectations": ["说明 thread_id 的作用", "引用官方资料"],
"actual": {"answer": "...", "citations": ["..."]},
"labels": ["missing_citation"],
"source_run_id": "run-123",
"source_version": "prompt-7/model-4"
}负反馈样本不等于正确答案,正反馈也不等于事实正确。每次调优应在同一批评估样本上比较答案质量、引用覆盖、工具次数、延迟和费用;线上反馈负责发现问题,离线评估负责判断候选改动是否值得发布。
用一次小改动验证闭环
调优不要同时改模型、提示词、检索器和 UI。先写一条可证伪的假设,例如“检索为空时先请求澄清,会减少引用不足的负反馈,但会增加一次交互延迟”。然后按照下面的顺序执行;如果结果没有改善,应该能指出是哪一个假设被否定,而不是只能说“新版本感觉不太好”:
查看 Mermaid 源码
flowchart TD
A[生产反馈] --> B[脱敏分桶]
B --> C[评估样本]
C --> D[一个修复假设]
D --> E[离线评估]
E -->|不通过| D
E -->|通过| F[小流量灰度]
F --> G[比较质量 / 延迟 / 成本]
G -->|改善| H[扩大并记录版本]
G -->|回归| I[停止或回滚]
H --> A
I --> D图下的判断不能只看点赞比例。灰度期间同时比较负反馈率、关键 Trace 的延迟和失败率、token / 费用以及人工复核结果;改动只有在目标指标改善且没有突破成本或安全边界时才扩大流量。
两条反馈闭环失败路径
用户重复点击,反馈数量被放大
先查 client_event_id、数据库唯一约束和网络重试日志。若同一次点击写入多行,说明幂等只停留在前端;处理方式是后端以 client_event_id 建唯一键,并让重复请求返回第一次结果。验证时连续提交同一请求,反馈表只能保留一条事实。
负反馈很多,却无法判断应该改什么
先看是否缺少 message_id、版本和 Trace 关联,以及自由文本是否被直接丢弃。若所有问题都落在 other,说明按钮采集了情绪却没有诊断入口;处理方式是补有限原因、保留脱敏评论,并人工抽样建立评估样本。验证时从一个样本可以打开 Trace、复现输入并说出下一步修复假设。
反馈系统的边界
用户反馈可能包含个人信息、恶意输入或偶然偏好,不能自动当作训练标签。高风险决策需要人工复核和权限控制;反馈接口也要限制频率,防止通过 run_id 枚举其他用户的答案。
闭环的终点不是“收到了反馈”,而是下一版系统能够用同一批样本和线上证据证明自己变好了。比如针对“引用不足”改了检索过滤器,至少要同时比较引用覆盖、负反馈率、延迟和成本;如果无法说清改动假设、评估结果和回滚条件,继续收集更多按钮只会增加噪声。
最小落地顺序
- 前端增加二元反馈和有限原因,携带
run_id、message_id、应用版本。 - 后端以
client_event_id幂等写入反馈表,并校验运行归属。 - Phoenix Trace 统一记录运行、节点、模型和工具的关联属性。
- 建立按原因、版本和任务类型切分的基础看板。
- 每周人工复盘高影响样本,形成第一版评估集。
- 每次只验证一个调优假设,先离线、再灰度,最后扩大或回滚。
参考资料
- Phoenix OTel SDK 配置(官方文档,访问日期:2026-08-24)
- OpenTelemetry 与 OpenInference(官方文档,访问日期:2026-08-24)
- LangGraph Streaming(官方文档,访问日期:2026-08-24)
Footnotes
-
Phoenix 官方文档:用于查询和关联 LLM、Agent 与工具调用的 Trace。 ↩