# 可编程 Scorer 使用指南 > 程序执行供本微服务 HTTP 与 CLI `--direct` 使用;第一阶段另提供显式 HTTP 自然语言生成接口,默认关闭。不提供独立 `/v1/rank` 或候选导出。qatlasd/网页的端到端接入需另行验证。 ## 1. 三种排序入口 | ranking | 行为 | |---|---| | 缺省或 null | 当前固定公式和默认权重 | | `"auto"` | 原线:服务调用配置的 LLM 选择三个权重;失败仍回退默认并记 intent 错误 | | `"scorer"` | 执行调用方提交的评分程序,不调用意图 LLM | `ranking` 是模式,`scorer` 是程序,不是一个权重参数。外部 Agent 可以阅读能力说明后编写程序,也可显式调用下文的生成接口;`/v1/search` 不会在 scorer 缺省时替它生成程序。 `agent: true` 仍只表示对最终结果生成总结;可以与 scorer 模式并用。 ## 2. 快速使用 ### HTTP 以下命令假设微服务已经部署且可达;将地址和令牌替换为自己的值。配置了服务令牌时,搜索与能力接口都要求 Bearer 鉴权。 ```bash curl -s http://localhost:8600/v1/scoring/capabilities \ -H 'Authorization: Bearer ' curl -s http://localhost:8600/v1/search \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "query": "surface code threshold", "sources": ["arxiv", "openalex", "semantic_scholar"], "ranking": "scorer", "scorer": { "language": "qatlas-expr-v1", "filter": "lexical >= 0.6", "score": "log1p(coalesce(citations, 0))" }, "explain": true, "max_results": 10 }' ``` ### CLI 从项目根目录执行: ```bash # 仅显示本地语法、特征和限额,不需要查询、配置或网络 qatlas-search --list-scoring-capabilities # 实际检索:读取本地 JSON 程序,直连已选数据源 qatlas-search --direct "surface code threshold" \ --ranking scorer --scorer-file docs/examples/scorers/classic.json --json qatlas-search --direct "surface code threshold" \ --ranking scorer --scorer-file docs/examples/scorers/recent.json --explain --json # 原有 LLM 配权重模式仍可使用;需服务 YAML 中配置 agent qatlas-search --direct "surface code threshold 最新进展" --ranking auto --json ``` - `--max-results` 仍为 CLI direct 的每源预算;`--top` 为展示条数。scorer 模式要求二者为正数。 - scorer 文件必须是 UTF-8 JSON,整个文件最多 4 KiB(含空白);读取在 JSON 解析前受限。 - scorer 模式的 `--json` 使用非缩进格式,防止美化输出扩大解释字节预算;旧默认/auto 输出格式不变。 - CLI 默认的 server 路径走 qatlasd,其 scorer/file/explain 参数在请求及鉴权解析前明确报不支持。不要用 `--server` 指向本微服务来绕过这一点,两者协议不同。 - CLI direct 的 `--agent` 不生成总结,沿用旧语义;需要总结用微服务 HTTP。 ## 自然语言生成(第一阶段) `POST /v1/scoring/generate` 只把自然语言要求转换为 `qatlas-expr-v1`,并使用现有编译器检查语法、类型和程序资源限制。它**不检索论文、不执行生成的 scorer、不自动搜索或优化**。检查程序、模型解释与警告后,由调用方把返回的 `scorer` 原样放入现有 `/v1/search` 请求,并指定 `ranking: "scorer"`;生成成功不等于运行时定义域安全或满足全部自然语言语义。 ### 请求与响应 请求对象只允许 `query`、`requirements` 两个必填字段;均为严格字符串,原始长度各为 **1..4000 字符**,不允许 null、类型转换或全空白,验证后去除首尾空白。不接受 `api_keys` 等额外字段。整个 UTF-8 JSON 请求体最多 **64 KiB**(含空白及分块传输);沿用解析前的编码及 JSON 嵌套保护。配置服务令牌时,先检查 Bearer 鉴权再读取请求体。 ```bash curl -s http://localhost:8600/v1/scoring/generate \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{"query":"surface code threshold","requirements":"只保留 lexical >= 0.6 的论文,按 log1p 引用数降序,缺失引用数按 0 处理"}' ``` 成功返回 HTTP 200: | 字段 | 契约 | |---|---| | `scorer` | 编译通过的规范化程序:`language`、`filter`、`score`;缺省 filter 补 `true` | | `summary` | 以 **`Model explanation: `** 开头的模型解释,不是证明或真实执行轨迹;模型正文最多 2000 字符 | | `warnings` | 模型警告字符串列表,最多 8 条,每条非空且最多 500 字符 | | `scorer_hash` | 与现有编译器一致的规范化程序哈希 | | `feature_version` | 当前特征版本 | | `usage` | `{"llm_tokens": N}`,本次最多两次 completion 的已知累计 token 用量 | 失败始终使用 `{"detail":{"code":"…","message":"…","field":"…"},"usage":{"llm_tokens":N}}`;`field` 可省略。错误不回显原请求、密钥或原始上游错误。已知用量即使后续修复失败或超时也保留;请求校验、预鉴权和准入拒绝均为 0。 | HTTP | 常见 code / 原因 | |---|---| | 400 | `request_encoding`:非 UTF-8 JSON 或字面 NUL | | 401 | `unauthorized`:缺失或错误令牌 | | 413 | `request_too_large`:请求体超限;`token_budget_exceeded`:预留或已报告用量超总预算 | | 422 | `invalid_request`、`request_nesting_limit`;`unsupported_requirements`、`clarification`;一次修复后仍非法的 `generation_invalid` | | 429 | `rate_limited`:进程内并发槽已满,`Retry-After: 1`,不排队 | | 502 | `model_response_too_large`、`model_response_invalid`、`model_error`、`model_unavailable` 或安全兜底 `generation_failed` | | 503 | `generation_unavailable`:未开启或共享模型配置不完整 | | 504 | `generation_timeout`:整次生成或网络超时 | 作者、期刊等不可用元数据不能转换成现有六特征公式。服务对 `requirements` 中常见作者/期刊/会议/机构/标识符/开放获取/影响因子等关键词**直接 fail closed**,不调用模型;该保守检测可能要求用户改写。对其他不支持或含糊要求,提示词要求模型明确返回 `unsupported_requirements` 或 `clarification`,不得静默丢弃约束或自造特征;用户需修改要求后重新请求。关键词检查和编译都不是完整的自然语言语义验证,仍需人工确认程序,不能把 `summary` 当作保证。 ### 内置变量与错误拒绝 `features` 中的键不是待用户定义的概念,而是服务器已经提供、可在 `filter` 和 `score` 中直接引用的标量变量。`score: "lexical"` 是完整合法的评分表达式,**不会再自动叠加 引用数、年份或默认评分公式**。`year` 是可用的年份变量;未知年份使用 `coalesce` 处理,并不意味着系统不支持年份过滤。例如“只要2020年之后,仅按 lexical 排序”可写为: ```json {"language":"qatlas-expr-v1","filter":"coalesce(year, 0) > 2020","score":"lexical"} ``` 这里不包含2020年,并排除未知年份;若包含2020年则使用 `>= 2020`。 生成提示词明确区分**引用已经计算好的 lexical/phrase 信号**与不支持的“新增原始文本 谓词/自造特征”,并包含相关性排序、严格年份边界及包含边界的完整示例。示例在离线 测试中经过模型输出 schema、编译和合成论文执行验证,而不是遇到失败时替换模型输出 的兜底规则。模型的 `unsupported_requirements` 解释仍是模型判断,**不是编译器对系统 能力的权威判定**;若模型拒绝,接口不会忽略要求或悄悄改用默认 scorer。 ### 生成语义验收(显式选择真实模型) 容器中可直接运行: ```bash # 默认纯离线:不读模型配置、不触网、不消耗模型 tokens。 docker exec qatlas-search python -m qatlas_search.scorer_acceptance # 明确选择真实模型:读取现有配置,提交固定的中文回归需求。 # 会产生模型费用;这是运维验收,不经过网页的用户额度计量。 docker exec qatlas-search python -m qatlas_search.scorer_acceptance --live ``` 固定需求为:“只要2020年之后,最终分数直接使用系统已有的 lexical 特征,按分数降序 排序,不加入引用数或其他加分项。”工具在生成后验证 **168 个合成样本**:严格排除 2019/2020/未知年份、保留2021年及以后;零/部分/全部词项相关性保持各自 lexical 分数, 改变引用数、年份和来源数不能引入加分。逐条命中及全量排序均与基准程序比较。 输出 JSON 含 `passed`、版本、机器 `code` 和已知 `usage.llm_tokens`;成功可显示 编译后的 scorer。失败返回非零退出码,不打印 key、base URL、模型原始解释或异常 堆栈。离线 `passed:true` **不代表真实模型生成成功**。`--live` 沿用现有最多两次 completion、30秒默认总限时和 token/响应预算,不添加外层重试,不检索论文或入库。 即使真实模式通过,也只是**固定需求的有限样本语义验收,不是所有需求/输入的证明**。 ### 启用与配置 `scorer_generation_enabled` 默认 `false`,独立于 `agent_enabled`。复用 `agent_base_url`、`agent_api_key`、`agent_model`,三者均须在 `strip()` 后非空;已有嵌套 `agent: {base_url, api_key, model}` 也会映射到这三个字段。仅开启旧 agent 不会开启生成;关闭旧 agent 也不妨碍单独开启生成。HTTP capabilities 增加 `generation_available` 表示上述条件是否满足,不代表上游连通性验证。 使用 DeepSeek 官方 endpoint 时,规则生成会自动采用非思考模式:仅当 `agent_base_url` 的 URL hostname 在大小写与末尾点规范化后**精确等于 `api.deepseek.com`**,每次 completion(含一次 repair)才在请求 JSON 顶层附加 `thinking: {"type": "disabled"}`;不添加 `reasoning_effort`。官方根地址 `https://api.deepseek.com` 与 `/v1` 地址及末尾斜杠均适用,**无需更换已有 API key 或 model**。这不是按模型名字判断:其他 OpenAI-compatible 服务、第三方网关或代理 host 即使使用 `deepseek-v4-pro`,也不附加该字段,保留其默认行为。此调整仅限规则生成,原 Agentic 结果总结(`agent: true`)请求保持不变。参数依据:[DeepSeek 官方 Thinking Mode 文档](https://api-docs.deepseek.com/guides/thinking_mode/)。 生成提示词使用 capabilities 的紧凑投影,仍保留全部可用特征/函数、nullable 与定义域约束,以及拒绝不支持要求和请求 clarification 的安全契约;公开 capabilities 不缩减。 以下字段均平铺于 YAML 的 `search:` 下,不使用嵌套 `scoring:`: | 字段 | 默认 | 允许范围 / 含义 | |---|---|---| | `scorer_generation_enabled` | `false` | 布尔值,显式开启生成 | | `scorer_generation_max_concurrency` | `2` | 1..32,生成请求并发槽 | | `scorer_search_max_concurrency` | `4` | 1..64,**全部** `/v1/search` 请求并发槽 | | `scorer_generation_timeout` | `30.0` | 1..120 秒,两次 completion 共用的总超时 | | `scorer_generation_max_tokens` | `1024` | 64..4096,**每次 completion 输出** token 上限 | | `scorer_generation_total_tokens` | `16384` | 1024..65536,整次生成**总输入 + 输出** token 预算 | | `scorer_generation_max_response_bytes` | `65536` | 1024..262144,每次上游原始响应字节上限,默认 64 KiB | 管理页 manifest 的 `scoring` 分组展示这些字段与说明;`PUT /v1/admin/config` 校验同样边界,拒绝 NaN/Inf。修改写回配置后**必须重启服务**,不会热更新预算或并发槽。生成通过运行时依赖 `httpx` 异步请求 OpenAI 兼容 `/chat/completions`,不依赖 OpenAI SDK。 ### 有界生成与计量 - 每次生成最多 **2 次模型调用**:一次初稿,只有输出 JSON/schema/DSL 无效时允许**单次 repair**。不对网络错误、不支持要求或 clarification 循环重试;修复不延长总超时,也不另开预算。 - 默认仍为 **30 秒整次生成总超时**,输出/总 token 及响应字节预算不变;这是生成处理阶段的期限,不是慢速客户端上传的全链路超时。官方非思考模式与提示词缩短不保证在期限内完成,也不是性能或延迟保证。 - 每次原始上游响应默认最多 **64 KiB**;拒绝压缩响应以避免解压膨胀。抽取的模型文本输出另外固定最多 **16 KiB UTF-8**,并继续受 scorer 4 KiB 及 summary/warnings 限制;这些不是整个 HTTP 成功响应的统一大小限额。 - 每次调用前,以序列化消息的 UTF-8 字节数加 256 framing 余量,再加本次最大输出 token,进行**byte-based 保守预留**。以累计预留与上游已知累计用量中的较大者检查剩余总预算;修复携带前次输出,也需重新预留,因此即使第一次调用可执行,也可能没有修复预算。 - 预留用于准入,**不是实际 token 统计**。`usage.llm_tokens` 只累计上游合法报告的 `total_tokens`,缺失时累计已知的 `prompt_tokens` / `completion_tokens`。未报告的未知 token 不得伪计为估算值;返回 0 或部分用量不代表上游没有成本,无法作为精确账单。已知超支返回 413,但不能撤销已经发生的模型调用。 ### 进程内立即准入 默认 `/v1/scoring/generate` 同时最多 **2** 个请求,**所有** `/v1/search`(包括默认、auto、scorer、multi)共用另一个上限 **4**。两组槽独立;即使生成关闭,search 上限仍生效。 纯 ASGI 准入在读取请求体、JSON 解析及同步线程池派发前非阻塞取槽,满额立即 **429**,不创建等待队列;正常、异常或取消退出均释放槽。它限制的是请求并发,不是每秒速率,也不是后端所有内部任务数;`/v1/survey-search` 不属于这两个槽组。 每个 ASGI app / worker 计数独立;多 worker / 多副本会使部署总并发随数量倍增,**没有分布式限流或跨进程全局配额**。需要部署级上限时须在网关或共享限流层另行控制。 ## 3. 程序格式 ```json { "language": "qatlas-expr-v1", "filter": "lexical >= 0.5", "score": "lexical * exp(-coalesce(age_years, 10) / 2)" } ``` - language、score 必填;filter 可选,默认 `true`。 - 字段必须是字符串,不接受未知字段或额外配置。 - filter 必须返回布尔值,score 必须返回非空有限数值。 - 先过滤,再评分;false 候选的 score 不执行。 - 服务对完整的合并候选集评分,不先按默认分数截断;按 score 降序,同分保持合并顺序,最后截取 max_results。 - 平台不强制词法优先、不限制权重、不强制归一化;但不会替程序补充不存在的特征。 参数冲突返回 400: - ranking=scorer 却缺少 scorer; - 提交了非 null scorer,却未选择 ranking=scorer; - ranking=scorer 与 mode=multi 同时使用。 multi 无自定义评分,原有合法 auto/agent 参数继续忽略。explain 默认为 false,必须是真正的 JSON 布尔值;默认/auto 不生成新增解释树。 ## 4. 六个特征 特征基于两轮检索汇总、身份去重和元数据合并后的论文,按实际执行路径惰性读取。 | 特征 | 类型 | 定义 | |---|---|---| | lexical | number | 当前未加权词项命中基础分,标题全权重、摘要半权重,0~1 | | phrase | number | 当前短语奖励,标题每短语0.5、摘要0.25;可累计超过1 | | citations | number 或 null | 累计引用数,负哨兵或未知为null,已报告的0保留0 | | year | number 或 null | 发表年份,缺失或非正值为null | | age_years | number 或 null | max(本次参考年份-year,0),无年份为null | | source_count | number | 合并后不同非空来源数量 | 词法/短语特征复用现有实现,不升级分词规则。空文本身份查询的 lexical、phrase 为0;若设置词法门槛导致身份命中被过滤,服务不擅自放宽。 不提供 raw_score/raw_rank、fame、任意字符串运算或跨候选集合统计。引用量不是“著名程度”的唯一含义,缺少的数据不会由公式凭空生成。 ## 5. 语法与类型 支持有限十进制数字(含小数/科学计数)、小写 true/false、特征名称、括号以及: ```text + - * / 算术及一元正负 < <= > >= == != 单次比较 not and or 布尔逻辑 ``` 优先级从高到低:括号/函数、一元正负、乘除、加减、比较、not、and、or。二元算术左结合。禁止链式比较,写成 `a < b and b < c`。 | 函数 | 约束 | |---|---| | min(a,b), max(a,b), abs(x) | 非空有限数值 | | clamp(x,lo,hi) | lo <= hi | | log1p(x) | ln(1+x),x > -1 | | sqrt(x) | x >= 0 | | exp(x) | 结果必须有限,溢出报错 | | coalesce(x,fallback) | fallback是非空同类值,只在x为空时执行 | | is_missing(x) | 返回是否为null;也允许传非空值 | | where(condition,a,b) | condition必须布尔;分支基类型一致,只执行选中分支 | 所有函数只接受固定数量的位置参数。and/or 短路执行,布尔不隐式转数字。没有循环、赋值、导入、属性、下标、自定义函数、幂、容器、反射、网络或文件访问。没有字符串、null字面量、注释、Unicode标识符、下划线数字或十六进制数字。 ### 缺失值要显式处理 ```text log1p(citations) # 422:citations可能为空 log1p(coalesce(citations, 0)) # 合法:作者明确选择缺失按0处理 ``` 编译器不做filter/分支的非空推导:即使filter已经检查引用非空,score中仍要写coalesce。where任一分支可能为空,则整个表达式也可能为空。 ## 6. 结果与解释 自定义 score 可以为负或大于1,保留浮点精度,不可当作概率,也不能自动跨程序比较。 响应 ranking 记录: - source=scorer,weights=null; - language、feature_version、scorer_hash; - 固定的 as_of_year; - score_scale=custom; - 实际生效的 scorer 程序(外侧空白已规范化,filter缺省补true)。 哈希由带语言/特征版本的规范化类型计划生成:空白、冗余括号、等价数字拼写不改变哈希;年份、查询和候选数据不在哈希内。 同程序、同版本、同查询、同合并记录和同参考年份才应得到相同结果。线上引用数、来源或年份变化可以改变分数。 ### explain=true 自定义 hit 的 score_detail=null;不伪造五分量账单。score_explanation来自同一次真实执行,包含参与特征、中间值、分支、缺失替代、候选序号和最终分数。 - 仅返回top hits的解释;不重新求值以生成解释。 - 解释列表JSON编码预算64KiB,采用无缩进序列化口径;这是解释数据预算,不是整个响应体预算。 - 执行器按候选/请求的top预算分配轨迹容量,有界保留top轨迹。 - 裁剪明确标记truncated;极紧预算时可能仅返回 `{"truncated": true}`,不影响分数。 - 默认/auto的score_detail及四位小数账单保持原样。 ## 7. 错误和资源限制 | 情况 | HTTP结果 | |---|---| | 缺少/错误服务令牌 | 401,搜索在读取请求体前鉴权 | | 模式冲突 | 400 | | 非UTF-8搜索JSON或字面NUL字节 | 400 | | 搜索请求体超过64KiB | 413,按实际收到的字节数检查,包括分块请求 | | scorer对象超过4KiB | 413 | | JSON容器嵌套超过64 | 422,在JSON完整解析前拒绝 | | 程序语法/类型/未知名称/节点深度超限 | 422,在任何数据源和LLM调用前拒绝 | | 合并后候选超过1000 | 422,不静默截断 | | 实际执行节点除零、定义域错误、非有限结果 | 422,带表达式位置和candidate_index,不返回半套排名 | | 全部被合法过滤 | 200,空hits,不自动放宽规则 | filter+score合计最多256个AST节点,深度最多32(包含表达式根、操作符及名称上下文);解析前另检查括号/容器语法嵌套32。具体计数口径可查询capabilities。 HTTP的scorer大小按所提交字段的紧凑UTF-8 JSON(不ASCII转义、表达式外空白规范化前)计算;CLI文件限制按实际文件字节。候选限额按真实合并集合计算,不能仅因各源预算之和超过1000就推断唯一候选必然超限。 scorer错误返回安全detail对象(code/message/field/可选position/candidate_index),不回显原始对象或配置。位置采用1起始、对应返回的有效表达式,结束位置不包含在范围内。 显式scorer错误不回退默认、不跳过坏候选、不调用后续总结。backend失败仍按原有errors按源报告;scorer和数据源错误是两套不同契约。 程序只在本次请求编译一次,无全局无界编译缓存。执行器解释不可变的白名单计划,不使用eval/exec或Python字节码执行。 ## 8. 离线测试与性能复测 从仓库根目录使用锁内开发依赖: ```bash uv sync --locked --extra dev uv run --no-sync --offline python -m pytest -q uv run --no-sync --offline python scripts/benchmark_scorers.py --repeats 5 ``` 测试使用固定论文和模拟数据源,不需要服务配置或外网。覆盖原有路径、自定义语言攻击输入、 CLI/HTTP结果一致性、短路/缺失、程序与请求预算、解释、身份查询和reinject。 性能脚本使用已经合并的100/1,000个候选,固定参考年份2026及explain_top=100;记录重复运行中位数、 独立编译成本和另一次执行的Python分配峰值。它不测上游网络、去重、HTTP链路或真实检索质量, 也不是延迟SLA。实际运行结果应随环境重新测量。 ## 9. 接入边界 `/v1/survey-search` 与本功能共存,但它不是 scorer 入口。该端点显式拒绝 顶层 ranking/scorer/explain(含null/false),不自动执行或生成评分程序。它与 `/v1/search` 共享请求体及预鉴权保护,其他规划/过滤/输出仍是独立协议,详见 [Survey 整合指南](survey-search.md)。 - 默认离线测试覆盖微服务HTTP(进程内TestClient)与CLI direct;不代表真实服务或第三方API已通过集成验证。 - 接入qatlasd/前端链路前需检查请求透传、ranking.source、自定义负分/大于1的分数和解释字段;发布本服务不自动保证该链路兼容。 - default_tools、服务令牌配置与部署网络假设不变;本功能不是对项目已有配置鉴权风险的全面整改。 - scorer只能重排已召回候选,不能改变源内召回和二轮白名单。 - 本服务不提供独立/rank、候选导出或自动优化程序循环。 版本、预发布和失败恢复统一遵循 [项目概览:版本与发布开发流程](overview.md#版本与发布开发流程),不在功能指南中单独维护另一套发版步骤。