极简产品实验失败后:怎样判断该删功能还是改假设
一次实验失败,不等于整个产品方向失败。先检查假设、样本和验收标准,再判断删功能、改交互还是换目标用户。记录“为何停止”比补一段励志总结更有用。
1. 为什么高 Recall 换不来用户留存
RAG(检索增强生成)系统在极简主义产品设计中是个双刃剑。极简主义的核心要求是“响应及时、信息精准、界面零干扰”。然而,为了追求稳定的召回率,工程师往往倾向于增加拼接上下文的层数:从 Dense Vector 检索,到 Sparse 关键字匹配,再叠加 Cohere 或 BGE Rerank 模型。
这种“堆料式”架构在生产环境会导致两个风险点:
- 上下文过载可能引发“中间失落”(Lost in the Middle):候选文档增多后,关键信息可能被无关片段稀释。应以不同 Top-K 档位回放同一评测集,比较引用准确率、回答质量、延迟和 Token 成本。
RAG 检索应优先保证响应体验:资源充足时使用完整检索,超时或依赖异常时逐级缩小范围或返回可解释的降级结果。
2. 问题现象与排查入口
实验失败后,可以在测试环境复现真实并发场景,使用autocannon压测 HTTP 接口,并配合qdrant-cli查看向量数据库的查询耗时。
# 使用 autocannon 模拟 20 并发持续压测 RAG 问答接口 autocannon -c 20 -d 10 -m POST -H "Content-Type: application/json" -b '{"query":"如何配置自动扣款?"}' http://localhost:8000/api/v1/search # 查看 Qdrant 向量检索点数与耗时分布 curl -X POST "http://localhost:6333/collections/knowledge_base/points/query" \ -H "Content-Type: application/json" \ -d '{"query_vector": [0.01, -0.02, ...], "limit": 10, "with_payload": true}'压测日志清晰反映了各模块的时延占比:
- 向量近似计算(Dense Vector Search):记录服务端检索耗时
- Cohere Rerank API 调用:记录外部调用与网络耗时
- 上下文 Prompt 组装与 Token 化:记录本地 CPU 耗时
- LLM 首包响应(TTFT):记录模型排队和首包耗时
整条链路累计耀对于一个定位极简、主打“即查即用”的创意工具而言,这个延迟无疑是问题性的。
3. 自适应 RAG 降级网关代码实现
针对 Rerank 导致的延迟膨胀,可以使用 Python/FastAPI 重写了检索网关,引入基于 Timeout 的自适应降级与流式截断策略。
import time import asyncio from typing import List, Dict, Any from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() class QueryRequest(BaseModel): query: str timeout_ms: int = 300 async def mock_vector_search(query: str) -> List[Dict[str, Any]]: await asyncio.sleep(0.08) # 80ms return [{"id": 1, "text": "自动扣款需在设置页绑定信用卡", "score": 0.89}, {"id": 2, "text": "扣款失败会在3天后重试", "score": 0.82}, {"id": 3, "text": "支持 Visa 和 Mastercard", "score": 0.75}] async def mock_rerank_service(docs: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # 模拟不稳定或耗时较长的 Rerank 服务 await asyncio.sleep(0.45) # 450ms docs.sort(key=lambda x: x["score"], reverse=True) return docs @app.post("/api/v2/adaptive-search") async def adaptive_search(req: QueryRequest): start_time = time.time() # 1. 基础向量检索 raw_docs = await mock_vector_search(req.query) # 2. 计算剩余时间预算 elapsed_ms = (time.time() - start_time) * 1000 remaining_budget = req.timeout_ms - elapsed_ms selected_docs = raw_docs degraded = False # 3. 动态判断是否执行 Rerank if remaining_budget > 200: try: # 带有硬超时的 Rerank 调用 selected_docs = await asyncio.wait_for( mock_rerank_service(raw_docs), timeout=remaining_budget / 1000.0 ) except asyncio.TimeoutError: degraded = True selected_docs = raw_docs[:2] # 超时后降级直接取 Top-2 else: degraded = True selected_docs = raw_docs[:2] total_latency = (time.time() - start_time) * 1000 return { "docs": selected_docs, "latency_ms": round(total_latency, 2), "degraded": degraded }4. 从技术语言到商业 ROI 的转换矩阵
为避免下一次 A/B 实验再次出现技术与产品认知的断层,可以建立一套技术指标到商业语言的翻译映射表:
| 算法/技术指标 | 研发关注视角 | 翻译后的商业语言 | 对用户与业务的实际影响 |
|---|---|---|---|
| Top-K Recall (召回率) | 记录研发关注视角 | 记录翻译后的商业语言 | 记录对用户与业务的实际影响 |
| Rerank 精度 | 记录研发关注视角 | 记录翻译后的商业语言 | 记录对用户与业务的实际影响 |
| Context Chunk Size | 记录研发关注视角 | 上下文更完整 | 单次 API 成本翻倍,LLM 输出变得啰嗦不干脆 |
| P99 延迟 (TTFT) | 记录研发关注视角 | 输入框回车后,下一帧及时吐字 | 记录对用户与业务的实际影响 |
5. 实验复盘后的避坑规则
一次失败的实验给团队留下的最大价值,不是那一堆废弃的代码,而是让所有人清醒认识到:用户买单的是解决问题的确定性与爽快感,而不是算法论文里的数学指标。
永远不要试图用复杂的架构去掩盖糟糕的响应延迟,极简产品设计的本质,恰恰是懂得在适当的时候做技术减法。