“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
RAG应用性能测试:完整工程指南
写在前面
如果你跟我一样是从JMeter或者k6背景过来的,第一次接触RAG(检索增强生成)端点时,第一反应肯定是直接扔一个负载测试上去,看看响应时间怎么样。这没错,但只做对了一半。
一个RAG应用完全可以给你一个又快又自信但完全错误的答案——普通的负载测试永远不会告诉你这个。你需要两个测试维度,不是一个:性能和质量。
这篇笔记会用一个贯穿始终的例子来展开:一个文档助手,回答"如何在非GUI模式下运行JMeter?"这个问题,基于一个小型的知识库。
为什么RAG打破了传统负载测试的假设
传统API返回完整响应,你测量往返时间就完事了。但RAG端点在回答之前干了两件昂贵的事:
- 检索上下文 - 从向量数据库或搜索索引中拉取相关文档
- 逐token生成响应 - 流式输出结果
第二步特别重要。单个请求可能流式输出几百个token,持续好几秒。所以"请求持续时间"这个单一数字掩盖了两个完全不同的问题:
- 模型开始回答花了多久(启动延迟)
- 模型生成起来后速度如何(生成速度)
一个启动慢但生成快的系统,在聊天UI里会让用户盯着空白屏幕发呆。一个启动快但生成慢的系统,问个简单问题还行,但要总结长文档就很痛苦。把这两个数字平均一下,什么都看不出来。
两个测试维度:性能和质量
我把RAG测试看作两个独立的关卡,只不过它们跑在同一个端点上。
性能回答的是:它有多快?负载下能不能扛住?这是k6的活儿,跟其他API负载测试一样,只是加上了LLM特有的指标。
质量回答的是:答案真的基于检索到的内容吗?还是模型自己瞎编的?这就是DeepEval上场的地方,用LLM作为裁判,对每次响应的忠实度和相关性打分。
单独任何一个关卡都不能告诉你完整的故事。一个快但胡言乱语的RAG应用比一个慢但准确的要糟糕得多;一个完全基于事实但需要8秒才能响应的应用,不管多准确都会流失用户。
真正重要的指标
性能指标
| 指标 | 含义 |
|---|---|
| TTFT(首token延迟) | 用户盯着空白屏幕多久才看到第一个字 |
| ITL(token间延迟) | 生成开始后,token流输出的流畅度 |
| Tokens/sec(每秒token数) | 生成速度,对长篇回答尤其重要 |
| p95 / p99 延迟 | 尾部体验,不是平均值 |
TTFT是整个系统里最影响用户体验的数字,也是传统负载测试工具最难单独测量的指标——因为它们是为原子化的请求/响应周期设计的,不是为流式输出设计的。
质量指标
| 指标 | 含义 |
|---|---|
| Faithfulness(忠实度) | 答案是否基于检索到的上下文,还是模型自己编的 |
| Answer Relevancy(答案相关性) | 答案是否真正回答了问题,还是听起来像那么回事 |
| Context Precision(上下文精确度) | 检索是否返回了正确的文档片段,且排序合理 |
| Context Recall(上下文召回率) | 检索是否遗漏了答案需要的任何内容 |
这四个指标承载了RAG评估中大部分诊断价值。忠实度和答案相关性属于生成侧;上下文精确度和召回率属于检索侧。
当忠实度低但上下文召回率高时,说明检索器干得不错,但模型没理它——这是提示词的问题,不是检索的问题。在调错组件之前,搞清楚这个区别很有价值。
用DeepEval检测幻觉
我选择DeepEval而不是RAGAS,主要是因为DeepEval把评估当作pytest测试用例来处理,带有通过/失败阈值——这正好是CI/CD关卡需要的形态。它还接受任何LLM作为裁判模型,所以不绑定某个厂商,尽管我们的示例应用用了Gemini。
下面是一个测试用例的样子,针对我们的JMeter文档助手示例:
from deepeval.metrics import FaithfulnessMetric, AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase
from deepeval.models import GeminiModel
judge_model = GeminiModel(
model="gemini-3.5-flash",
api_key=os.getenv("GEMINI_API_KEY"),
)
faithfulness_metric = FaithfulnessMetric(threshold=0.75, model=judge_model)
answer_relevancy_metric = AnswerRelevancyMetric(threshold=0.8, model=judge_model)
def test_jmeter_non_gui_mode_answer():
question = "How do I run JMeter in non-GUI mode?"
result = query_rag_app(question)
test_case = LLMTestCase(
input=question,
actual_output=result["answer"],
retrieval_context=result["retrieved_chunks"],
)
for metric in [faithfulness_metric, answer_relevancy_metric]:
metric.measure(test_case)
status = "PASS" if metric.success else "FAIL"
print(f"[{status}] {metric.__class__.__name__}: {metric.score:.3f}")
failed = [m for m in [faithfulness_metric, answer_relevancy_metric] if not m.success]
if failed:
names = ", ".join(m.__class__.__name__ for m in failed)
raise AssertionError(f"Metrics below threshold: {names}")
用 pytest 运行这个测试,它要么通过要么失败,跟其他任何测试一样。这就是关键——它把一个模糊的"AI说得对不对"的问题,变成了一个二进制的CI/CD信号。
测试套件还包括重试逻辑,用于处理Gemini API的临时503错误,自动重试最多3次,带指数退避。DeepEval同时生成JUnit XML和HTML报告,可以轻松接入任何理解pytest输出的CI系统。
用k6做负载测试(以及为什么你暂时测不了TTFT)
这里有个让人沮丧的地方:如果你来这里是想找到干净利落的TTFT测量方案,那我要告诉你——k6的SSE扩展(xk6-sse)跟k6 v2不兼容。它只支持 go.k6.io/k6 v1,在它更新之前,你只能在k6 v2的改进架构和正确测量流式指标之间二选一。
所以配套的仓库做了务实的选择:测试 /chat/complete 端点而不是 /chat 流式端点,用k6内置的 http 模块。不需要自定义二进制文件,不需要扩展,就是标准k6。
代价是你失去了真正的TTFT测量,因为 /chat/complete 会等完整响应返回。你得到的是端到端延迟,这仍然有用——它能告诉你系统是否慢,只是不能告诉你为什么慢。
测试长这样:
import http from 'k6/http';
import { Trend, Counter } from 'k6/metrics';
import { check } from 'k6';
const totalDuration = new Trend('total_duration_ms', true);
const tokensPerSecond = new Trend('tokens_per_second');
const BASE_URL = __ENV.RAG_APP_URL || 'http://localhost:8080';
export const options = {
scenarios: {
rag_chat: {
executor: 'ramping-vus',
stages: [
{ duration: '30s', target: 10 },
{ duration: '1m', target: 10 },
{ duration: '30s', target: 0 },
],
},
},
thresholds: {
'total_duration_ms': ['p(95)<5000'],
'tokens_per_second': ['avg>10'],
},
};
export default function () {
const payload = JSON.stringify({
question: "How do I run JMeter in non-GUI mode?",
});
const params = {
headers: { 'Content-Type': 'application/json' },
};
const res = http.post(`${BASE_URL}/chat/complete`, payload, params);
check(res, {
'status is 200': (r) => r.status === 200,
});
if (res.status === 200) {
const body = JSON.parse(res.body);
totalDuration.add(res.timings.duration);
// 估算每秒token数
const tokenCount = body.answer.split(' ').length;
const durationSec = res.timings.duration / 1000;
tokensPerSecond.add(tokenCount / durationSec);
}
}
注意阈值设置:total_duration_ms 的p95要小于5000ms,tokens_per_second 的平均值要大于10。这些值需要根据你的实际情况调整,但至少给了你一个硬性门槛。
把两件事串起来:CI/CD流水线
现在我们有两条测试轨道了:
- 质量测试 - 用DeepEval + pytest,检查忠实度和相关性
- 性能测试 - 用k6,检查端到端延迟和吞吐量
把它们放进GitHub Actions流水线,这样每次PR都能自动跑:
name: RAG Performance & Quality Tests
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install deepeval pytest
pip install -r requirements.txt
- name: Run quality tests
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
RAG_APP_URL: ${{ secrets.RAG_APP_URL }}
run: |
pytest tests/test_quality.py --junitxml=quality-results.xml
- name: Upload quality results
if: always()
uses: actions/upload-artifact@v3
with:
name: quality-results
path: quality-results.xml
performance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install k6
run: |
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Run performance tests
env:
RAG_APP_URL: ${{ secrets.RAG_APP_URL }}
run: |
k6 run tests/load-test.js --out json=performance-results.json
- name: Upload performance results
if: always()
uses: actions/upload-artifact@v3
with:
name: performance-results
path: performance-results.json
这里的关键点是两个job是并行跑的,互不依赖。性能测试不需要等质量测试完成,反之亦然。这样整个流水线跑得最快。
实际运行中的注意事项
1. 测试数据集的选择
不要只用一个问题去测。你需要一个覆盖常见场景的测试集:
- 简单问题 - 可以直接从检索到的文档中找到答案
- 复杂问题 - 需要综合多个文档片段才能回答
- 边缘问题 - 知识库中只有部分相关或不相关的信息
- 无答案问题 - 知识库中完全没有相关信息
每个问题都应该有预期的忠实度阈值和相关性阈值。对于无答案问题,好的RAG系统应该返回"我不知道"而不是瞎编。
2. 裁判模型的选择
DeepEval使用LLM作为裁判,但裁判模型本身也会犯错。建议:
- 使用能力更强的模型作为裁判(比如Gemini 3.5 Flash或GPT-4)
- 不要用同一个模型既做应用又做裁判,否则会引入系统性偏差
- 如果预算允许,对裁判结果做人工抽样验证
3. 阈值设置的艺术
阈值不是拍脑袋定的。建议先跑一轮基线测试,然后根据业务需求调整:
- 忠实度0.75 - 这意味着允许25%的"自由发挥",对于创意写作可能可以,对于事实性问答太宽松了
- 相关性0.8 - 20%的"跑题"空间,对于客服场景可能太高了
- p95延迟<5000ms - 5秒对于对话式应用来说已经偏长了,理想情况下应该在2-3秒内
实际项目中,这些阈值需要跟业务方一起确定,并且随着系统迭代不断调整。
4. 失败处理
质量测试中的AssertionError会导致pytest返回非零退出码,CI流水线就会标记为失败。这意味着:
- 如果模型开始胡言乱语(忠实度低),PR会被阻止合并
- 如果模型答非所问(相关性低),PR也会被阻止
- 如果检索器坏了(上下文召回率低),同样会被阻止
这就是你想要的——在代码进入生产环境之前,抓住所有可能让用户失望的问题。
总结一下
RAG应用的性能测试不能只靠传统的负载测试工具。你需要:
- 性能测试 - 用k6测量端到端延迟、吞吐量,未来如果能测TTFT就更好了
- 质量测试 - 用DeepEval测量忠实度、相关性、上下文精确度和召回率
- CI/CD集成 - 把两者都放进流水线,让每次PR都自动验证
两条轨道都通过了,才能说这个RAG应用"准备好上线了"。少任何一条,你都是在赌——赌模型不会突然发疯,赌用户不会因为慢而离开。
别赌,测。