qatlas-search — QuantumAtlas 的多源学术检索微服务

面向学术检索的独立微服务:客户端发请求,服务端做 multi-source fan-out (arXiv / OpenAlex / Semantic Scholar / Crossref / qatlasd 论文目录 / qatlas-rag 语义检索),按 精准词项匹配 + 引用量排序;可选地再让 LLM 对 top hits 写一段结论。pipeline 刻意简单:不加 agent 就直接返回命中列表。

与向量 RAG(qatlas_rag)并列的另一条检索路径:RAG 走向量相似度,本服务走 严格文本匹配(标题 / 摘要 / 元数据精准词项)+ 年均引用率加权,无需 GPU、 无需向量库。qatlas-rag 服务也可以作为一个 fan-out 源接入本服务(rag backend, 默认关闭,见下文"对 qatlas-rag 的依赖")。

架构

客户端 (qatlas-search CLI / qatlas 插件 / web)
        │
        ▼
   qatlasd ── 用户 auth、计量、身份锚定都在这里完成
        │  Authorization: Bearer <search.remote.token>
        ▼
 qatlas-search (本服务) ── fan-out 到各数据源 → 融合排序,可选 LLM 总结
        │                 (fused 混合勾选时,网页命中经 reinject 二轮反喂学术源)
        │
        ▼
 学术源: arXiv / OpenAlex / Semantic Scholar / Crossref / PubMed /
   Europe PMC / DBLP / DOAJ / OpenAIRE / CORE / NASA ADS / Springer /
   IEEE Xplore / Scopus / qatlasd 论文目录 (catalog) / qatlas-rag (rag)
 网页源: Wikipedia / SearXNG(自托管) / Tavily / Exa / Serper / Brave / Kagi

本服务只接受内部网络请求(compose 示例不做任何 ports 映射),用共享的 service_token 校验调用方;用户级鉴权、配额计量、论文身份锚定均由 qatlasd 负责。

接口协议

POST /v1/search
  请求:  {"query": str = "", "max_results": int = 10,
          "sources": [str] | null, "agent": bool = false,
          "ranking": "auto" | "scorer" | null,
          "scorer": {"language", "filter"?, "score"} | null,
          "explain": bool = false,
          "mode": "fused" | "multi" = "fused",
          "api_keys": {backend: key} | null,
          "arxiv_id": str | null, "doi": str | null, "title": str | null}
  响应(fused 默认/auto): {"hits": [{title, abstract, authors, year, doi, arxiv_id, url,
                    venue, citations, source, score, score_detail,
                    raw_rank, raw_score}],
          "conclusion": str | null,
          "ranking": {"source": "llm"|"default",
                      "weights": {...} | null},
          "usage": {"llm_tokens": int},
          "errors": {backend|intent|agent|reinject: msg}}
  响应(multi):  {"results": {backend: [hit, ...]},   # 每个平台一份原始
                                          # 命中列表,不做跨源合并/排序
          "conclusion": null,
          "usage": {"llm_tokens": 0},
          "errors": {backend: msg}}

GET /v1/backends → {"backends": [{name, label, category,
                    requires_key, user_key, available}]}
                  # backend 目录:qatlasd 据此渲染前端复选框;
                  # available 只反映服务端 key/配置,不含请求级 key

POST /v1/survey-search → 有界多查询规划及规则过滤(独立接口,不接受 scorer 参数)

GET /v1/scoring/capabilities → 版本化评分语法、特征、函数、资源限额、示例及 generation_available

POST /v1/scoring/generate
  请求: {"query": str, "requirements": str}  # 严格字符串,各 1..4000 字符且不可空白
  成功: {"scorer": {"language", "filter", "score"},
         "summary": "Model explanation: …", "warnings": [str],
         "scorer_hash": str, "feature_version": str, "usage": {"llm_tokens": int}}
  失败: {"detail": {"code": str, "message": str, "field"?: str},
         "usage": {"llm_tokens": int}}       # 保留已知累计用量,不伪计未知 token
  # 显式开启,生成并编译但不执行;确认 scorer 后另调现有 /v1/search

GET /healthz → {"status": "ok", "agent_configured": bool,
                "backends": {name: ready bool}}

GET /v1/admin/manifest → 配置清单:分节字段(label/type/description)、
                         当前值(secret 字段打码)、healthz 同款 status
PUT /v1/admin/config   → {"updates": {key: value}},写回实际加载的 YAML
                         配置文件的 search: 段(agent.* 写回嵌套 agent: 子段),
                         需重启生效
  • 默认/auto 的 score_detail 是 score 的"账单":五个加权分量(lexical / phrase / citation / recency / agreement),之和恒等于 score。打分为绝对分—— 固定查询、权重、合并记录与年份时,不依赖其他候选;引用分量按年均引用率(log 饱和)计, 时效分量为 5 年半衰期衰减。multi 模式的原始命中不打分(score 恒 0)。

  • ranking: "auto"(可选,仅 fused 模式)开启逐请求排序意图:服务端用一次 LLM 调用(复用下文 agent: 端点配置)从查询里抽取权重 θ,与 fan-out 并行、夹紧后供本次打分;失败静默回退代码默认权重并记 errors["intent"], 响应 ranking 块标明实际生效的权重来源。

  • sources 为 null 时用配置里的 default_tools;只会选择当前可用的 backend (服务端有 key/配置,或请求 api_keys 里带了对应 key)。

  • 请求了但不可用(缺 key / 缺服务端配置)的 backend 记入 errors,不再静默丢弃。

  • max_results 夹取到 1..100;multi 模式下每个 backend 的预算为 min(max_results, max_results_per_tool)。

  • api_keys 由 qatlasd 注入(当前用户在网页端配置的第三方 key),只覆盖对应 backend,服务端 YAML 里的同名 key 是兜底;本服务不持久化任何请求级 key。

  • mode: "multi" 时忽略 agent 与合法的 ranking: "auto"(没有融合后的 top 集合); 不支持 ranking: "scorer",该组合返回 400。

  • 身份查询(v0.4.0):query 为空而 arxiv_id / doi 在场时走精确取回而非 空搜索 fan-out——只调能解析身份的源(arXiv id_list、OpenAlex filter=doi:、 Semantic Scholar paper/arXiv:… / paper/DOI:… 单对象端点),其余源静默跳过 (不发请求、不记 error);query 与身份全空但有 title 时按标题搜索;四者 全空返回 400。query 非空时身份字段不参与,行为与从前一致。

  • reinject 二轮检索(v0.5.0,仅 fused):sources 同时含网页源与学术源时, 网页命中不进融合排名,而是被解析成 ≤3 条二轮查询——链接里的 arXiv/DOI 标识符经身份端点精确取回,网页标题做成引号短语——只打一轮勾选的学术源 (sources 白名单不扩大;标识符无归属源时丢弃、名额让给标题通道)。发现的 论文并入结果按用户原始查询统一打分。纯网页勾选不触发二轮(网页命中直接 参与排名);二轮失败记 errors["reinject"],不影响一轮结果。开关为 reinject.py 的 REINJECT_ENABLED 代码常量。

  • 配置了 search.service_token 时,/v1/search、/v1/survey-search、/v1/backends、 /v1/scoring/capabilities、/v1/scoring/generate 与 /v1/admin/* 校验 Authorization: Bearer <token>(与 qatlasd 侧 search.remote.token 同值), 不符返回 401;/healthz 始终开放,供探针使用。

  • /v1/admin/* 供 qatlasd 管理页代理调用:manifest 中 secret 字段永远打码, 更新里填打码串 / null / 空串表示"不修改";配置文件缺失或只读挂载时 PUT 返回 500。

  • agent: true 且服务端已配置 LLM 时,返回里多一段 conclusion;agent 失败不会 拖垮响应,而是记入 errors["agent"]。

可编程 Scorer(首版)

排序模式明确分为三条:缺省/null 用默认公式;ranking: "auto" 保留原 LLM 配权重路径; ranking: "scorer" 执行调用方提交的受限程序,不自动调用 LLM 写程序。 第一阶段另提供显式 POST /v1/scoring/generate:默认关闭,只生成并编译检查程序, 不检索或执行;调用方确认后再将返回的 scorer 提交现有 /v1/search。

{
  "query": "surface code threshold",
  "ranking": "scorer",
  "scorer": {
    "language": "qatlas-expr-v1",
    "filter": "lexical >= 0.6",
    "score": "log1p(coalesce(citations, 0))"
  },
  "explain": true
}
  • 六个特征:lexical、phrase、citations、year、age_years、source_count。 它们是已绑定的标量变量,可在 filter/score 中直接引用;score: "lexical" 合法且不自动加入其他评分项。 生成提示词含经过编译/执行回归的完整示例,明确支持 coalesce(year, 0) > 2020 等年份过滤。 模型错误拒绝不会被静默替换为示例或默认程序。

  • python -m qatlas_search.scorer_acceptance 默认纯离线检查固定需求的168个合成样本; 显式加 --live 才使用当前配置调用真实模型(有费用,不经过网页额度),再做相同的有限样本 验收。离线成功不等于真实模型成功,也不证明全部自然语言需求;详见 Scorer 指南。

  • 支持算术、门槛、条件分支和非线性函数;nullable 特征必须显式处理,不强制词法优先。

  • scorer 在完整合并候选上执行,再截 top-k;保留身份路由和网页反喂规则。

  • 自定义 score 为有限浮点值,不强制 0~1、不舍入,score_detail 为 null; explain: true 增加有界的真实执行解释。响应 ranking 返回实际程序、哈希、特征版本及参考年份。

  • 程序错误明确返回 413/422,不回退默认;模式冲突返回 400。

  • 搜索请求体限 64 KiB、UTF-8 JSON、容器嵌套 64;scorer 对象限 4 KiB、256 AST 节点、 深度 32;自定义评分最多 1,000 个合并候选,解释 JSON 列表预算 64 KiB。

  • 本仓提供微服务 HTTP 和 CLI direct 入口;默认 server CLI 会拒绝 scorer/file/explain 参数。qatlasd/网页的端到端接入需另行验证;本服务不提供独立重排、 候选导出或生成后自动执行/优化循环。

  • 生成请求仅接受严格的 query / requirements,各 1..4000 字符且不可空白,整个 body 限 64 KiB。 成功含 scorer、带 Model explanation: 标签的 summary、warnings、scorer_hash、 feature_version 与 usage;失败为安全 detail 对象及已知累计 usage。

  • 作者/期刊等不支持的 requirements 直接 fail closed;其他不支持或含糊要求交由模型明确拒绝或 clarification,不得静默丢弃。编译不是自然语言语义证明,程序仍需调用方审核。

  • 生成最多两次 completion(一次修复),默认总超时 30 秒;每次 raw 响应默认限 64 KiB, 抽取的输出文本固定限 16 KiB。总输入+输出预算使用 byte-based 保守预留并结合已知上游用量, 未知 token 不得伪计为实际 usage。

  • 进程内生成请求默认上限 2;全部 /v1/search(含默认/auto/scorer/multi)共用另一上限 4。 ASGI 层在读取 body、解析 JSON、同步线程池派发前立即取槽,满额 429,不排队。 每 worker / app 独立,多 worker / 副本使总额度倍增;不是分布式限流,也不覆盖 survey。

完整生成契约、错误码、预算与部署限制见 自然语言生成(第一阶段)。

qatlas-search --list-scoring-capabilities
qatlas-search --direct "surface code threshold" --ranking scorer \
  --scorer-file docs/examples/scorers/classic.json --explain --json

完整类型、短路规则、错误码及接入说明见 Scorer 使用指南, 另有 近期论文、高引用经典、 年均引用 程序示例。

配置

独立的 YAML(不依赖 qatlas 主包),按顺序取第一个存在的文件:

  1. ~/.qatlas/config.yaml(单机 / 本地开发)

  2. /etc/qatlas-search/config.yaml(容器挂载点)

与 qatlasd 同机部署时:qatlasd 对 config.yaml 里的未知 key 严格报错, 两者不能共用一个文件。把本服务的配置写到独立的 ~/.qatlas/search.yaml, 挂载为容器的 /etc/qatlas-search/config.yaml(见 deploy/docker-compose.example.yaml)。

只读顶层的 search: 段;缺文件 / 缺段不报错,用内置默认值:

search:
  # --- 第三方数据源(均可选)---
  semantic_scholar_api_key:        # 提高 S2 速率上限(也可由用户 key 注入)
  openalex_api_key:                # OpenAlex 免费账号 key(匿名额度已收紧)
  openalex_email: you@example.com  # 进 OpenAlex polite pool
  crossref_email: you@example.com  # 进 Crossref polite pool
  pubmed_api_key:                  # 可选;NCBI 限速 3→10 req/s

  # --- 需 key 的 backend(服务端兜底 key;qatlasd 正常会注入用户自己的 key)---
  core_api_key:
  ads_api_key:                     # NASA ADS
  springer_api_key:
  ieee_api_key:
  scopus_api_key:
  tavily_api_key:                  # 以下为网页引擎
  exa_api_key:
  serper_api_key:
  brave_api_key:
  kagi_api_key:
  searxng_url: http://searxng:8080 # 自托管 SearXNG(需在实例上开启 json 输出)

  # --- catalog backend(qatlasd 论文目录)---
  server_url: http://qatlasd:8090
  token: <qatlasd-side token>

  # --- rag backend(qatlas-rag 语义检索;默认关闭,详见下文"对 qatlas-rag 的依赖")---
  rag:
    enabled: true
    url: http://qatlas-rag:8801
    token: <qatlas-rag-side token>
    timeout: 30.0                # 可选;缺省回落到 request_timeout

  # --- 服务层 ---
  service_token: <与 qatlasd 的 search.remote.token 同值>
  service_port: 8600

  # --- 可选 LLM 总结(OpenAI 兼容端点;也可平铺成 agent_enabled 等字段)---
  agent:
    enabled: true
    base_url: https://llm.example.com/v1
    api_key: sk-...
    model: my-model
    max_tokens: 1024

  # --- 独立 Scorer 生成(平铺字段;复用 agent 的 base_url/api_key/model)---
  scorer_generation_enabled: false       # 不依赖 agent.enabled
  scorer_generation_max_concurrency: 2   # 1..32
  scorer_search_max_concurrency: 4       # 1..64,全部 /v1/search
  scorer_generation_timeout: 30.0        # 1..120 秒,整次生成总超时
  scorer_generation_max_tokens: 1024     # 64..4096,每次 completion 输出
  scorer_generation_total_tokens: 16384  # 1024..65536,总输入+输出预算
  scorer_generation_max_response_bytes: 65536  # 1024..262144,每次 raw 响应

  # --- 工具选择 ---
  default_tools: arxiv,openalex,semantic_scholar,catalog
  max_results_per_tool: 10
  request_timeout: 20.0         # 每阶段等待上限;HTTP 调用受剩余预算约束
  search_timeout: 50.0          # /v1/search 各联网阶段共享的总等待预算

搜索等待预算与部分结果

/v1/search 的首轮 fan-out、可选网页反喂(reinject)和 LLM 总结共享 search_timeout(默认 50 秒)。每阶段最多等待 request_timeout(默认 20 秒),且不得超过整次搜索剩余预算;HTTP 重试、429 的退避、Semantic Scholar 的限速排队以及反喂的多条派生查询均消耗原有预算,不会每次重置。默认预算给 qatlasd 的 60 秒 remote timeout 留出余量。若部署自定义主服务超时,应让它明显大于 search_timeout,预留排序、论文登记和响应传输时间;这不是 CPU/数据库处理的硬超时。 新增 search_timeout 须为 (0, 300] 内有限秒数,旧 request_timeout 的配置兼容性 不变。YAML 中无效的 search_timeout 只回退该字段到 50 秒,不丢弃其余配置及 鉴权 token;admin API 对无效值仍拒绝写入。写入 YAML 后需重启 Search 服务。

慢源不再阻止已有结果返回:超时/工作线程饱和记录在 errors[backend],可选阶段 则记录在 errors.reinject / errors.intent / errors.agent。第二轮后续查询超时 不会丢弃该源已完成的派生查询结果;总结超时仍返回论文列表。HTTP 200 仍可表示部分 成功(甚至全空但有 errors),前端必须展示错误,不应将它当作穷尽结果或自动重试。 这不会绕过第三方 429 限流,也不改变 qatlasd 的配额计量策略。

同步 requests 已开始的网络 I/O 无法强行取消。整个进程共用至多 32 个联网工作 槽,超时返回后仍运行的任务继续占槽,直至真正结束;饱和时拒绝新增工作并显式报错, 不为每次请求无限增建线程。预算到期后不再发起后续重试或派生查询。迟到结果不再合入 已返回的响应;迟到的 LLM 调用仍可能由供应商计费,其未在预算内取得的 token 用量 无法回填到已经返回的响应中。该预算不覆盖 /v1/survey-search 的整次多查询流程; 其每次调用检索引擎受限,但总时限仍由该端点单独管理。

启用 qatlas_search logger 的 INFO 级别可查看 search_stage 的阶段耗时,以及 search_completed 的总耗时、结果数和失败阶段数;这些统计不记录查询文本、API key 或模型提示词。Uvicorn 默认 access log 的 200 OK 本身不能证明在主服务超时前完成。

默认公式的排序权重是代码级配置:打分逻辑与权重(lexical 1.0 / citation 0.6 / recency 0.2)定义在 src/qatlas_search/scoring.py,不通过 YAML 或 /v1/admin/* 修改——YAML 里写 weight_* 会被当作未知 key 忽略。逐请求的 权重调整用 ranking: "auto"(见上文接口协议)。

Scorer 生成需独立开启 scorer_generation_enabled,且共享的 agent_base_url、 agent_api_key、agent_model 在 strip() 后均非空;不要求 agent_enabled=true。 /v1/admin/manifest 的 scoring section 展示开关、预算及说明,admin 更新验证上述边界并 拒绝 NaN/Inf;写回 YAML 后需重启,不能热更新并发槽。字段均平铺在 search: 下。

DeepSeek 官方 endpoint 的 Scorer 生成自动使用非思考模式:agent_base_url 的 URL hostname 经大小写和末尾点规范化后精确匹配 api.deepseek.com 时,初稿与 repair 的 JSON 顶层均增加 thinking: {"type": "disabled"},不加 reasoning_effort。根地址、/v1 及末尾斜杠均支持,不需改已有 key/model;非官方 OpenAI-compatible 网关/代理 host 保留默认,即使 model 为 deepseek-v4-pro 也不按模型名启用此参数。原 Agentic 总结 payload 不变。依据:DeepSeek 官方 Thinking Mode。紧凑生成提示词仍由 capabilities 投影,完整保留可用 features/functions、拒绝/clarification、nullable 与 domain 约束;默认总超时仍为 30 秒,预算不变,非思考模式和提示词缩短均不是性能保证。

旧 LLM 总结/配权重路径使用 requests;Scorer 生成使用运行时依赖 httpx 的异步客户端 请求 {base_url}/chat/completions。均不依赖 openai / anthropic SDK。

对 qatlas-rag 的依赖

rag backend 由独立部署的 qatlas-rag 服务提供(GPU 节点,bge-m3 hybrid 检索本地 论文语料),本服务只作为调用方接入它,不内嵌任何向量检索实现。qatlas-rag 提供的 接口协议为:POST {url}/v1/retrieve(Authorization: Bearer {token},请求体 {"query": str, "max_results": int},响应 {"hits": [...], "errors": {...}}, hit 字段与本服务 /v1/search 的 hit 一致且 source 为 "rag"),另有 GET {url}/healthz 供就绪探测。

接入方法是在配置里声明依赖(见上文配置示例的 rag: 段):enabled: true 加上 url 与 token 三者齐备时 backend 才可用,/healthz 的 backends.rag 就绪位 据此报告;未启用或配置不全时该位为 false,行为与其他可选 backend(如 catalog) 一致。timeout 可单独覆盖 rag 请求的超时,缺省回落到 request_timeout。rag 的 超时、非 2xx 与网络错误都记入响应的 errors["rag"],不影响其他源的命中。注意 default_tools 默认不含 rag:启用后还需把 rag 加进 default_tools,或在 请求的 sources 里显式指定。

版本化说明:本服务自 v0.2.0 起提供 rag backend,并要求 qatlas-rag 侧实施上述 /v1/retrieve 接口协议。各仓独立版本化、独立部署;本仓没有声明支持的 qatlasd / qatlas-rag 软件版本范围,也没有业务调用前的软件版本兼容握手。 qatlasd 当前只检查本服务的 HTTP 健康状态;固定镜像 tag、健康检查通过或服务端 版本号相同都不等于兼容性已验证。升级仍需核对 API/语义、调用方测试及实际集成。

本地开发

使用 Python 3.11+(CI / Docker 为 3.12)、uv 0.11.30:

uv sync --locked --extra dev
uv run --no-sync --offline ruff check .
uv run --no-sync --offline python -m pytest
uv lock --check
uv run --no-sync uvicorn qatlas_search.service.app:app --port 8600

首次 sync 可下载锁内依赖;测试执行默认不联网,不读取个人配置 / netrc。 真实外部 API 用例必须显式 uv run --no-sync python -m pytest -m network。 轻量 socket guard 保留 mock、TestClient、localhost 与 Unix socket,不是系统网络沙箱, 不能约束子进程或原生库;新增测试仍应显式 mock 外部调用。

版本与发布开发流程

pyproject.toml 的 [project].version 是唯一版本来源;运行时从已安装发行包的 importlib.metadata 取版本(先 sync / 安装项目,不直接依赖未安装的 src)。 Commitizen 的 uv provider 同步 pyproject.toml、uv.lock,version_files 同步根 VERSION。 已发布 tag 仍指向其原始代码 / workflow;后续 CI 的防重发检查不会追溯保护旧 run。 不要通过移动旧 tag、覆盖旧产物或重跑旧版本来应用新的发行流程。

在干净且同步的默认分支 master 上,先预览:

git switch master
git pull --ff-only
uv sync --locked --extra dev
uv run --locked --extra dev cz bump --dry-run

只有获得单独发版授权后才执行:

uv run --locked --extra dev cz bump
git push origin master --follow-tags

日常提交使用 Conventional Commits(feat: / fix: / docs: 等)。cz bump 在本地 增量更新 CHANGELOG、版本与锁文件,跑 Ruff/pytest hook,然后创建版本提交和 annotated v$version tag;不会自行 push。hook 失败时可能已改文件但尚未 commit/tag:先检查差异 并修复,勿盲目重跑。不要裸跑 cz changelog 重生成历史。保留完整 ## vX.Y.Z (YYYY-MM-DD) 标题与非空正文;预发布使用规范 PEP 440(如 0.9.0rc1 / 0.9.0.dev1,不使用 epoch/local),转正需审查是否汇总了预发布的重要说明。

预发布使用下面的 RC 路径代替上面的正式 bump,不能依次执行两套流程:

uv run --locked --extra dev cz bump --prerelease rc --dry-run
# 核对推导出的完整版本与发版授权一致后,才执行并推送:
uv run --locked --extra dev cz bump --prerelease rc
git push origin master --follow-tags

预发布也会真实创建版本提交、tag 并触发镜像上传,不是模拟测试。发布后需检查 Actions 中的测试、镜像与 Release jobs,并核对 GHCR tag / digest、Release 正文及 预发布标记。下载或部署时从 Releases 选择已验证的完整 tag;RC 需显式选择用于测试,不视为稳定版推荐或生产部署授权。

新增 backend(开发者必读)

  • 失败契约(v0.5.0 起):search(query, settings) -> (papers, error)——返回 (list[Paper], str | None) 元组;任何网络/解析失败返回 ([], 原因) 而非抛 异常。错误只走返回值:实例属性 last_error 已删除(并发请求共享注册表 单例,错误随返回值走才能保证归属正确)。

  • 类属性:name / label(UI 展示名)/ category(academic | web)/ requires_key + key_setting(该源 key 在 Settings 里的字段名,请求级 api_keys 的注入槽)/ cost_tier。能按标识符精确取回的源再设 supports_identity = True,并在 search() 开头处理 query.identity_only—— 不适用的身份种类返回 ([], None) 静默跳过("帮不上忙"不是"失败")。

  • HTTP 一律经基类 _get / _post(共享重试/退避与 User-Agent);响应解析放 _parse。

  • 测试约定:mock base.requests.request 断言请求形状;成功路径必须断言 err is None。参考 tests/test_identity_search.py、tests/test_reinject.py。

  • 要加新的执行阶段,参考 reinject.py:目标 backend 由调用方传入(不碰全局 注册表)、同源顺序 + 跨源并行、错误逐查询聚合、engine.run_direct 只留一行 接线。

CLI

qatlas-search 默认是 server 模式:POST 到 qatlasd 的 /api/search/agentic(由 qatlasd 代理到本服务)。server/token 解析顺序: --server/--token flag → 插件 CliContext(作为 qatlas search 挂载时, 协议 v2)→ qatlas 客户端 auth(qatlas auth login,惰性 import, 没装 qatlas 也不报错)→ ~/.config/qatlas/config.yaml 顶层 server_url/token。 HTTP 调用优先走 qatlas 客户端的 pluginsupport 公共层(PAT 注入、 版本协商头、TLS 选项)。

qatlas-search "surface code threshold"            # server 模式,默认带 agent 总结
qatlas-search "VQE ansatz" --no-agent --max-results 5
qatlas-search --list-tools                        # 列出 backend 及本地可用性

自动化检索的结果与失败判定

普通搜索(server 和 --direct)返回 0 表示有可用结果,或检索正常结束但没有命中; 返回 1 表示请求失败,或结果为空且报告了检索错误;用法/配置错误返回 2。 有结果的部分失败保持 0,调用方仍应检查 errors,不能据此声称已穷尽文献。 这一退出码约定用于普通搜索;survey / multi 保留各自协议。

Direct JSON 除 query / results 外包含 errors、per_backend_counts 和 reinject_counts;每篇保留 abstract、raw_rank 和 raw_score(未知为 null)。 计数来自后端原始返回,可能因合并、过滤和 --top 与输出条数不同。 server JSON 保留服务端原始响应,错误警告也输出到 stderr。 server 文本输出同时显示 qatlas_id=qa_…,可直接用于 qatlas paper get markdown; direct 模式不生成统一 ID,需先用 qatlas match 或服务端搜索核对身份。

survey / multi 子命令(均走 qatlasd)

# 规则化综述检索(POST /api/search/survey):覆盖为 post-filter bounded
qatlas search survey "topological qubits" \
  --author "Felix von Oppenheim" --year-from 2020 --min-citations 50
qatlas search survey "surface codes" --query "surface code threshold" \
  --query "mbqec" --sort citations --json

# 逐平台原始结果(POST /api/search/multi):各平台自身排序,无融合
qatlas search multi "quantum error correction" --sources arxiv,openalex

--direct 模式

qatlas-search --direct '"quantum supremacy" sampling' --tools arxiv,openalex --json

--direct 在本进程内直连各学术数据源(engine.run_direct)。注意其性质: 自带 key、不经过 qatlasd、不计量、不做身份锚定——适合本地开发与调试, 不作为受控环境的入口。

部署

镜像由本仓 release 流水线发布到 GHCR(见 .github/workflows/release.yml),不发布 PyPI。 master push / PR / v* tag / 手动运行均先跑锁内 Ruff 和默认离线测试;只有 tag ref 通过测试、精确版本和 CHANGELOG 校验后才发布。手动选择分支只测试。 镜像仅写 ghcr.io/iai-ustc-quantum/qatlas-search:v版本,不写裸版本或 latest, 保留 linux/amd64 + linux/arm64、QEMU、provenance / SBOM 和 GitHub attestation (私有仓功能权限不足时 attestation 仍为 best-effort)。镜像先于 GitHub Release。 获得部署授权后,部署机只 pull 不 build。将下面占位值替换为 Release 页面中 已经核对过的完整 tag(包含 v),不要照抄历史示例或使用 latest:

cp deploy/docker-compose.example.yaml deploy/docker-compose.yaml
export QATLAS_SEARCH_VERSION='v<已验证的版本号>'
docker compose -f deploy/docker-compose.yaml pull
docker compose -f deploy/docker-compose.yaml up -d

固定 tag 并记录镜像 digest 有助于定位产物,但不保证可回滚。回滚还取决于旧镜像 可获取、配置/数据变更兼容以及实际回滚验证;固定版本也不能代替跨服务兼容性测试。

新版本遵循上面的 Commitizen 流程,不能手改 VERSION 再随手打 tag。 CI 在真实认证后查询 GHCR:仅标准“manifest/name 不存在”响应允许首次 build/push; 已有 tag 会明确停止并给出 digest,网络 / 401 / 5xx / 无法判定均停止,不当成不存在。 同 ref workflow 串行,但 GHCR 不提供原子 compare-and-push;其他发布者也必须遵守 不可覆盖约定,不能并发从外部改写同名 tag。

部分产物失败只恢复、不重发:镜像与 Release 是独立 jobs。镜像成功而 Release 失败时,在同一次 run 选择 Re-run failed jobs,只补 Release,不重建镜像。 不要选择 Re-run all jobs:整次重跑会被已有 tag 的 preflight 拦住。镜像已存在而 run 不可恢复时,先核对原始 tag、镜像 digest 与对应 CHANGELOG,只人工补缺失的 Release; 不要删除/移动 tag、覆盖镜像,或重建同版本冒充原产物。需要改产物则走新的授权版本。 发布与部署分别授权,发布完成不自动更新生产。

示例 compose 把服务接入外部 shared-infra 网络,不做 ports 映射(只允许 网络内访问,流量一律经 qatlasd 进入),并以只读方式挂载 本服务独立的 ~/.qatlas/search.yaml 到 /etc/qatlas-search/config.yaml。

本地开发时仍可以 docker build -t qatlas-search:local . 自建镜像,但这不是 发布路径——线上只允许来自 GHCR 的镜像。Docker 用 uv 0.11.30 按 uv.lock 安装 build group(hatchling 及传递依赖),禁用隔离构建并离线构建当前项目 wheel;随后 sync 仅生产依赖并安装该 wheel,运行阶段保留非 root 用户、不带开发/构建工具。 这不宣称普通 uv build 的隔离依赖天然受锁约束;离开该流程时须另行约束构建工具。 基础 Python / uv 镜像 tag 仍可能被上游更新,锁文件不等于整张镜像逐字节可复现。

已发布镜像的只读验证

将 verify-release.yml 合入默认分支后,在 Actions 的 Verify released image 页面 选择 master,输入已存在的完整版本 tag;也可执行:

gh workflow run verify-release.yml --ref master -f tag='v<已发布版本号>'

该流程只用仓库 GITHUB_TOKEN 的包读取权限,拉取 amd64 镜像,校验 index 包含 linux/amd64 与 linux/arm64,然后按记录的 digest 在断网、只读容器中检查安装包版本。 结果和 digest 写入 run summary;不会重建/发布镜像或改写 tag/Release。这只是 CPU 包 metadata smoke,不代表 arm64 运行、真实搜索 API、GPU 或端到端兼容性测试通过。

测试

uv sync --locked --extra dev
uv run --no-sync --offline python -m pytest -q

离线覆盖:各 backend 的 _parse(固定 fixture)、身份查询(arXiv id_list / OpenAlex DOI / S2 paper 端点形状与 identity-only fan-out 收窄)、reinject (混合门控 / 白名单 / 路由 / 错误聚合 / 契约守卫)、(papers, error) 元组契约 (成功路径断言 err is None)、catalog 与 rag 的请求形状(mock requests)、 rag 的故障隔离与就绪位、排序与去重、config 加载(含嵌套 agent/rag 段拍平)、 agent(mock LLM 端点)、FastAPI 服务(TestClient + 假 backend:命中形状 / agent 开关 / token 401 / healthz)。带 network 标记的 live 用例默认跳过。