local-deep-research 端到端性能评测实战:live-service 测试与人工评测 harness 完全指南
【免费下载链接】local-deep-research~95% on SimpleQA (e.g. Qwen3.6-27B on a 3090). Supports all local and cloud LLMs (llama.cpp, Ollama, Google, ...). 10+ search engines - arXiv, PubMed, your private documents. Everything Local & Encrypted.项目地址: https://gitcode.com/GitHub_Trending/lo/local-deep-research
tests/performance/是 local-deep-research 项目面向真实外部服务(arXiv、OpenAlex、Ollama 等)的实时端到端测试与人工评测(eval)工具箱:这里不关心"函数是否返回正确类型",而关心"整条研究流水线在真实网络上跑起来效果如何"。读完本文你将掌握:如何在本机运行这套 live-service 测试与评测脚本、如何解读 KEPT/REMOVED 过滤决策与跨模型/跨提示词对比结果、如何通过run_full_search.py一键生成可复现的 Quick Summary 报告,以及如何为新的子系统扩展同类评测。
一、定位与设计哲学:为什么单独存在一个 performance 测试目录
与仓库内大量使用 mock 的单元测试不同,tests/performance/下的代码真实命中外部服务并测量端到端行为。这意味着它天然具有不确定性——arXiv 可能限流、Ollama 可能未启动、网页可能改版——因此这套测试被刻意排除在 CI 之外。
CI 排除机制有两层(详见 .github/workflows/docker-tests.yml):
- pytest 标记过滤:CI 运行
pytest -m 'not integration ...',所有被打上integration标记的测试都不会进入 CI; - 命名约定:非
test_*.py的eval_*.py、run_*.py、build_*.py评测脚本根本不会被 pytest 收集,它们是被当作普通 Python 程序直接执行的。
这套"测试 + 评测"双轨设计的核心理念是:pass/fail 断言只能证明"没崩",而人工阅读输出才能判断"好不好"。因此该目录下的产物主要分为两类:
| 类型 | 典型文件 | 成功信号 |
|---|---|---|
| pytest 集成测试 | test_live.py、test_extraction_benchmark.py、test_new_adapters_integration.py等 | 断言通过,同时print()输出供人审阅 |
| 人工评测 harness | eval_prompt.py、eval_models.py、run_full_search.py、build_eval_dataset.py | 没有断言,人类阅读输出并做出判断 |
二、目录布局:一个子系统一个评测文件夹
以下是 README 规划的完整目录结构(当前仓库实际存在_shared、relevance_filter、content_fetcher、search_engines、database、api_auth等文件夹,strategies等目录按同一规范逐步落盘):
tests/performance/ ├── _shared/ — 通用管线 harness,不绑定单一子系统 │ ├── run_full_search.py — 围绕 quick_summary() 的薄 CLI;--engine / --model 参数 │ └── build_eval_dataset.py — 查询 × 引擎 × 模型的叉积批量运行器(驱动 run_full_search.py) ├── relevance_filter/ — LLM-as-judge 相关性过滤(实时 arXiv + Ollama) │ ├── test_live.py — pytest 实时 arXiv + Ollama 测试(integration + requires_llm + slow) │ ├── eval_prompt.py — 人工判断 harness:固定 judge 与 arXiv,切换提示词变体 │ └── eval_models.py — 人工判断 harness:固定提示词与 arXiv,切换 judge 模型 ├── strategies/ — 分解 / 迭代推理策略对比(实时 LLM + meta_search) │ └── compare_strategies_visual.py — 人工判断 harness:跨策略时间线可视化绘图 ├── content_fetcher/ — 200+ 真实 URL 的 HTML 提取质量基准(实时网络) ├── search_engines/ — 新适配器对接实时 API 的集成测试(Open Library、Zenodo 等) ├── mcp/ — MCP 客户端并发测试(真实 subprocess echo server) ├── database/ — 加密数据库向后兼容(安装上一版 PyPI 发行版) └── api_auth/ — 带认证的研究 API 校验(需运行中的服务器 + Puppeteer)值得注意的扩展规范(README 原文要求):为某个新子系统新增性能测试/评测时,应在其旁边创建与relevance_filter/平级的兄弟文件夹(例如 embeddings、rate limiting、search-engine latency),而任何真正与子系统无关的通用内容放入_shared/。
三、运行前准备:环境变量是这套评测的开关
tests/performance/的几乎所有脚本都通过环境变量控制行为,默认值面向"典型本地开发机":
| 环境变量 | 默认值 | 作用 |
|---|---|---|
LDR_TESTING_WITH_MOCKS | true(见 tests/conftest.py) | 必须设为false,否则任何带requires_llm标记的测试会被自动跳过 |
LDR_TEST_OLLAMA_BASE_URL | http://localhost:11434 | Ollama 服务地址 |
LDR_TEST_OLLAMA_MODEL | qwen3.5:9b | 默认 judge/生成模型标签 |
LDR_TEST_ARXIV_MAX | 40 | 每次查询拉取的 arXiv 结果条数 |
LDR_TEST_OLLAMA_MODELS | (无) | eval_models.py专用:逗号分隔覆盖默认模型集合 |
LDR_BOOTSTRAP_ALLOW_UNENCRYPTED | (无) | 本机无 TLS 环境下允许 Ollama 明文连接,dev 脚本均要求true |
LDR_APP_DEBUG | (无) | 置为true开启 DEBUG 日志,可看到过滤器的 KEPT/REMOVED 决策 |
其中最关键的是第一个:tests/conftest.py将LDR_TESTING_WITH_MOCKS默认设为true,这会让所有requires_llm测试在未显式覆盖时自动跳过——这正是 README 强调"运行本套测试必须显式传false"的原因。
四、运行 pytest 集成测试:命令与编写规范
4.1 标准运行命令
在仓库根目录执行:
LDR_TESTING_WITH_MOCKS=false \ LDR_TEST_OLLAMA_BASE_URL=http://localhost:11434 \ LDR_TEST_OLLAMA_MODEL=qwen3.5:9b \ pdm run pytest tests/performance/ -v -s -m integration-s用于显示测试内print()的输出——这是审阅决策质量的关键;-m integration只选中实时测试(slow、requires_llm子集可另行组合选择)。
4.2 pytest 测试的三条硬性规范
README 明确要求本目录下所有 pytest 测试必须遵守:
- 打标记:携带
@pytest.mark.integration,通常还要加@pytest.mark.requires_llm和@pytest.mark.slow,从而保证被排除在 CI 之外(CI 用-m 'not integration'过滤); - 优雅跳过而非失败:当所依赖的外部服务不可达时应
skip,而不是fail,保证在没有本地 Ollama 端点时跑本地性能套件不会红一片; - 用
print()输出决策:打印 KEPT/REMOVED(或等价细节),配合-s供人审阅质量。
以 relevance_filter/test_live.py 为例:其ollama_llmfixture 先通过safe_get(带allow_private_ips=True,允许访问用户配置的本地/私有网络端点)探测/api/tags,不可达即pytest.skip,可达才惰性导入langchain_ollama.ChatOllama构造真实 judge(num_ctx=8192)。随后对两条精心挑选的查询("LLM interpretability latest research"、"sparse autoencoders for mechanistic interpretability")参数化运行filter_previews_for_relevance,打印 KEPT/REMOVED 清单,并设置两条"软断言"兜底:
assert kept:过滤器不能把 40 条结果全拒掉(judge 坏了或提示词过度拒绝);assert len(kept) < len(previews):过滤器不能全保留(提示词已失去过滤作用)。
这两条软断言刻意宽松——测试的主要价值是"把决策浮出水面给人看",而非用严格阈值钳死行为。
五、人工评测 harness:提示词与 judge 的可控变量实验
eval_*.py脚本是人类判断工具:没有断言,成功信号是"人读输出后做出判断"。它们的共同设计是每次查询只抓取一次 arXiv 结果,然后只切换单一变量,保证对比干净。
5.1 eval_prompt.py:固定 judge,切换提示词
运行方式(eval_prompt.py):
LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=true \ LDR_TEST_OLLAMA_BASE_URL=http://localhost:11434 \ LDR_TEST_OLLAMA_MODEL=qwen3.5:9b \ pdm run python tests/performance/relevance_filter/eval_prompt.py脚本围绕同一个共享模板脚手架展开,各变体只替换中间一段 guidance,查询块、日期块、结果块与输出契约完全一致,从而隔离提示词措辞这一唯一变量:
def _make_template(guidance: str) -> str: return ( "This is a relevance-filtering step. Kept results move forward ...\n\n" 'Query: "{query}"\n' "Current date: {current_date}\n\n" "Search results:\n" "{preview_text}\n\n" f"{guidance}\n\n" "Output ONLY the 0-based indices of relevant results as a comma-separated list, nothing else.\n" "Example: 0, 2, 5" )内置的 5 个提示词变体各自对应一段真实历史教训:
| 变体 | guidance 要点 | 背景 |
|---|---|---|
V0_current | 直接主题匹配比关键词匹配更重要 | 当前 main 分支基线(选择性偏差修复后),用于验证新变体 |
V1_prefer_smaller | 倾向更小的高置信度选择 | 曾因引入选择性偏差被移除,保留以量化回归 |
V2_inclusive_adjacent | 保留主题密切相关的工作,拒绝仅共享关键词者 | 针对"只共享查询词之一导致的假阴性" |
V3_keep_framing | 反过来强调"保留什么" | 阅读它是否有助于回答查询;边缘相关也值得保留 |
V4_terse | 更简洁,接近历史 "MUST directly address" 表述 | 纯关键词重叠不属于相关 |
执行时每个变体以batch_size=10、max_parallel_batches=4调用 relevance_filter.py 的filter_previews_for_relevance,最后输出三组对比统计:
- Per-variant counts:每个变体保留了 40 条中的多少;
- Consensus(全部变体都保留)与Rejected by ALL(全部变体都拒绝):稳定性锚点;
- Disagreements(部分保留部分拒绝):真正值得人眼逐个审阅的争议项。
5.2 eval_models.py:固定提示词,切换 judge
运行方式(eval_models.py):
LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=true \ LDR_TEST_OLLAMA_BASE_URL=http://localhost:11434 \ pdm run python tests/performance/relevance_filter/eval_models.py默认模型集合按"家族 × 尺寸多样性"挑选,用于回答"提示词质量能否跨模型泛化":
qwen3:4b—— 最小模型,测试能力下限;qwen3.5:9b—— 当前默认;gemma3:12b—— Gemma 家族;ministral-3:14b—— Mistral 家族;gpt-oss:20b—— OpenAI 系开源模型;qwen3.5:27b—— 更大的 Qwen,观察规模是否带来增益。
脚本会先通过/api/tags枚举 Ollama 已安装模型,自动跳过未安装的标签(可通过LDR_TEST_OLLAMA_MODELS覆盖模型列表),然后逐模型记录 keep 数量与耗时,输出与 5.1 相同的 Consensus / Rejected by ALL / Disagreements 分析,外加每个模型的运行秒数,方便在"判断质量"与"推理成本"之间做权衡。
六、一键端到端评测:run_full_search.py 生成 Quick Summary 报告
_shared/run_full_search.py 是围绕项目编程接口quick_summary()的薄 CLI:产出与 Web UI "Quick Summary" 模式同风格的报告,但只需一条命令,从而可以用同一查询在不同引擎之间做 diff(例如 arxiv vs openalex),用于在真实场景中评估相关性过滤提示词与综合生成质量。
6.1 用法与全部参数
LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=true LDR_TESTING_WITH_MOCKS=false \ pdm run python tests/performance/_shared/run_full_search.py \ --query "LLM interpretability latest research" \ --engine openalex \ --output /tmp/ldr_report_openalex.md参数表(均带默认值,--query必填):
| 参数 | 默认值 | 说明 |
|---|---|---|
--query | (必填) | 研究查询 |
--engine | arxiv | 搜索引擎,可选arxiv/openalex/wikipedia/searxng |
--model | qwen3.5:9b(或LDR_TEST_OLLAMA_MODEL) | Ollama 模型标签 |
--ollama-url | http://localhost:11434 | Ollama 端点 |
--iterations | 1 | 对应search.iterations,与 REST API 的 Quick Summary 默认一致 |
--output | /tmp/ldr_report_<engine>.md | 报告输出路径 |
--verbose | 关 | 开启LDR_APP_DEBUG=true,显示过滤器的 KEPT/REMOVED 决策 |
6.2 底层实现:从探活到加密写盘
该脚本的实现细节揭示了整条评测管线的关键环节:
- 启动前探活:
check_ollama()用safe_get(超时 5s、allow_private_ips=True)探测{url}/api/tags,不可达则向 stderr 报错并返回退出码 1——这是所有 dev 脚本共用的"优雅降级"模式; - 设置快照:通过
create_settings_snapshot()一次性覆盖llm.provider=ollama、llm.model、llm.ollama.url、search.tool、search.iterations、api.allow_file_output=True等键(见 run_full_search.py),以programmatic_mode=True调用quick_summary(); - 报告头元数据:输出文件以 Markdown 头记录
Engine、Model、Iterations、Sources(来源数量)、Elapsed(耗时秒数)、Generated(生成时间)——这段头正是后续build_eval_dataset.py解析"来源数"的契约; - 加密写盘:文件最终经
write_file_verified()落盘(对应api.allow_file_output配置门控),与 Web UI 的文件输出走同一安全路径。
七、批量评测数据集构建:build_eval_dataset.py
_shared/build_eval_dataset.py 把 6.1 的脚本提升到"实验矩阵"级别:对每个(查询 × 引擎 × 模型)单元格端到端运行quick_summary,同时保存格式化报告与包含 KEPT/REMOVED 决策的详细日志,最后汇总成 CSV 供按来源数、耗时排序并目视检查离群值。
LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=true LDR_TESTING_WITH_MOCKS=false \ pdm run python tests/performance/_shared/build_eval_dataset.py \ --output-dir ./ldr_eval_output默认配置即产生一个4 查询 × 2 引擎 × 3 模型 = 12 单元格的网格(README 注明在qwen3.5:9b上全程约 60–90 分钟):
| 轴 | 默认值 | 覆盖方式 |
|---|---|---|
--queries | LLM interpretability 相关 4 条(如 "mechanistic interpretability of transformer language models"、"sparse autoencoders for neural network feature discovery"、"safety alignment and refusal in large language models") | "q1|q2"(|分隔,因查询内可能含逗号) |
--engines | arxiv,openalex | 逗号分隔 |
--models | qwen3.5:9b,gemma3:12b,ministral-3:14b | 逗号分隔 |
--output-dir | ./ldr_eval_output | 落reports/、logs/、summary.csv |
--parallel | 1 | 并发单元格数;慎用——每个并发槽都会在 Ollama 端加载一个 LLM,同时加载 2+ 个不同模型标签可能 OOM;同模型并行通常安全(Ollama 排队) |
--force | 关 | 强制重跑已存在报告文件的单元格 |
该脚本在工程上有三个值得借鉴的细节:
- 可断点续跑(resumable):
already_done()检查reports/<key>.md是否存在,存在即跳过;--force可重跑,用于从被打断的运行中恢复或扩展数据集; - 子进程隔离:每个单元格以
subprocess.run独立调用run_full_search.py,保证单元格间互不污染,并设置 30 分钟硬超时;超时或异常时删除半截报告文件,避免下次运行误判"已完成"而跳过; - 增量 CSV 汇总:
summary.csv按(query, engine, model)upsert,跑完一行写一行,实时可查,字段含sources、elapsed_s、exit_code、report_path、log_path。
八、其他子系统的实时评测矩阵
8.1 content_fetcher:200+ 真实 URL 的提取质量基准
test_extraction_benchmark.py 对 200+ 个来自新闻、技术、学术、政府、教育、购物、社交、金融、国际站点等十余个类目的真实页面跑提取管线(源码注释口径为 17 个类目,ALL_CATEGORIES字典实际含 misc 杂项共 18 个分组),并对比两种下载模式:
- 静态下载器(
HTMLDownloader,超时 20s):无需 JS 渲染的常规页面; - 自动下载器(
AutoHTMLDownloader,超时 20s,基准中显式enable_js_rendering=True以覆盖 JS 渲染回退路径):面向 JS 重型页面。
每个页面记录内容长度、boilerplate 命中数(13 个关键词:cookie、sign up、newsletter、subscribe、accept all、privacy policy、log in、add to cart……)与耗时,按类目打印成功/失败的进度条与平均值。断言刻意宽松以容忍反爬、地理封锁与付费墙:静态子集允许 20% 失败率,全量基准要求整体成功率不低于 60%。同一文件还验证了ContentFetcher的 URL 路由正确性——arXiv、PubMed、PMC、Semantic Scholar、OpenAlex、bioRxiv 学术 URL 应分别被路由到专用下载器。
8.2 search_engines:新适配器的实时 API 集成
test_new_adapters_integration.py 覆盖 5 个新增搜索适配器:Open Library、Project Gutenberg、Zenodo、Stack Exchange、PubChem。每个适配器验证:
- 字段契约:结果必须含
title、link,且source为期望值(如 "Open Library"、"Project Gutenberg"、"Stack Overflow"); - 领域特有字段:Stack Exchange 的
score(整数)与tags、PubChem 的molecular_formula/molecular_weight/cid/smiles、Zenodo 的doi; - 全内容路径:
search_snippets_only=False时结果应含content字段且清理掉_raw。
最值得关注的是TestFactoryIntegration类:它不 mock 安全白名单,而是走完整生产路径settings snapshot → search_config() → get_safe_module_class()(安全白名单)→ 引擎实例化,验证新适配器确实已在 module_whitelist.py 中登记(ALLOWED_MODULE_PATHS/ALLOWED_CLASS_NAMES)。若工厂返回 None,最可能的原因就是白名单缺失——这为"新增引擎"提供了明确的可执行检查清单。
8.3 database:加密数据库的向后兼容
test_backwards_compatibility.py 守护加密存储的兼容性:数据库由旧版创建、新版必须仍能打开,防止 salt 修改之类的破坏性变更悄悄引入。该文件包含两类测试:
- 快路径(CI 内):加密常量稳定性测试(README 注明已迁移至
test_encryption_constants.py),能捕获约 99% 的破坏性变更; - 慢路径(人工触发):
TestBackwardsCompatibility通过pip index versions local-deep-research获取上一版本号,在隔离 venv中安装上一版 PyPI 发行版并生成数据库,再以当前代码打开、读取写入的UserSettings记录验证数据完整。该测试标记slow并受RUN_SLOW_TESTS=true门控(完整运行需pytest ... -m slow),总超时 10 分钟——绝大多数时间耗在依赖安装上。
8.4 api_auth:带认证的研究 API 校验
test_research_validation.py 依赖运行中的服务器 + Puppeteer 认证(见同目录conftest.py),验证/api/start_research接口行为:缺query、空query应返回 400/422;有query但缺省/空model应返回 200(走数据库默认模型);未认证请求应被拒绝(400/401)。它代表的是"完整部署环境下的验收测试"这一评测场景。
九、快速上手决策表:什么场景用哪个工具
| 目标 | 使用工具 | 关键标志 |
|---|---|---|
| 快速验证相关性过滤器没崩、决策合理 | pytest tests/performance/relevance_filter/test_live.py -v -s | 打印 KEPT/REMOVED,两条软断言兜底 |
| 比较多个提示词措辞 | eval_prompt.py | 固定 judge,5 个变体一次跑完 |
| 比较多个本地 LLM 的判断力 | eval_models.py | 固定提示词,自动跳过未安装模型 |
| 生成一份可复现的 Quick Summary 报告 | run_full_search.py | 单条命令,报告含来源数与耗时元数据 |
| 构建跨引擎/跨模型的评测数据集 | build_eval_dataset.py | 12 单元格默认网格,可断点续跑 |
| 验证新增搜索适配器 | test_new_adapters_integration.py | 字段契约 + 工厂安全白名单路径 |
| 守护加密数据库兼容性 | test_backwards_compatibility.py | CI 快路径 + 隔离 venv 慢路径 |
| 验收带认证的 research API | test_research_validation.py | 需运行中的服务器 + Puppeteer |
十、扩展指南:为新子系统添加性能评测
遵循 README 的规范,当你要为 embeddings、rate limiting、search-engine latency 等新子系统添加评测时:
- 新建平级兄弟文件夹,如
tests/performance/embeddings/,不要堆进relevance_filter/或其它既有目录; - 通用、与子系统无关的 harness 放
_shared/(如新的批量 runner); - pytest 测试记得三件套标记(
integration+requires_llm/slow)、外部服务不可达时skip、用print()输出决策细节供人审阅; - 纯评测脚本(无断言)用
run_*.py/eval_*.py命名,避免被 pytest 误收集。
这套"实时集成测试 + 人工评测 harness + 可复现报告"的组合,正是 local-deep-research 在本地 LLM 与多搜索引擎组合下持续保障研究质量的关键基础设施。
【免费下载链接】local-deep-research~95% on SimpleQA (e.g. Qwen3.6-27B on a 3090). Supports all local and cloud LLMs (llama.cpp, Ollama, Google, ...). 10+ search engines - arXiv, PubMed, your private documents. Everything Local & Encrypted.项目地址: https://gitcode.com/GitHub_Trending/lo/local-deep-research
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考