Skip to content

系统架构

PaperLoom 同时运行两条生命周期完全不同的链路。一条把 PDF 建成长期保存的 Reading Model,另一条 在单次会话授权范围内启动 Agent,检索、阅读、引用并提交答案。把两条链路混在一起,很容易把 “仓库里存着什么”误写成“当前回答用到了什么”。

研究链路遵守一条硬规则:论文内容声明必须连接到当前会话有权访问、并且 Agent 已经读取过的原文 证据。 权限、工具状态、Evidence Ledger、最终校验和历史引用共同维护这条约束。

系统架构图

PaperLoom 当前系统架构图

图里有三个运行边界:

代码范围持有的状态和职责不负责什么
Folio论文选择、结构化点击锚点、研究进度、答案与引用重开不从回答文本反推权限或来源身份
Java / Spring Services用户权限、SourceScope、配额、会话、Corpus API、Qdrant 索引合同与维护状态、MySQL 准确读取、Reference Mapping不替模型选择搜索词和阅读位置
harness_py授权范围内的 Agent Loop、Corpus Gateway、工具状态、Evidence 与提交校验不保存全文索引,不扩大用户论文范围,也不持久化产品权限

Java 保存产品必须长期信任的状态,Python 保存一次研究回合里不断变化的状态。OpenAI Agents SDK 负责通用的 Model / Tool Loop;工具能改变哪些集合、什么结果可以结束 Run,都由项目代码处理。

两条生命周期

Reading Model 构建链

text
Research PDF
-> MinerU parser artifact
-> PaperReadingModelBuilder
-> Pages / Sections / Typed Elements / Locations / Relationships
-> MySQL canonical Reading Model
-> Qdrant rebuildable candidate projection
-> MinIO PDF / parser artifacts / screenshots / crops

这条链路在首次上传和受控 Parser 迁移时运行。Parser Artifact 会被保留,方便重建和排障;产品长期依赖的是 PaperReadingModelBuilder 产出的版本化模型。换 Parser、调整检索或增加派生索引时,论文在产品里的 身份不需要跟着改变。

论文访问与可检索状态

text
file_upload(user_id, paper_id)
-> 论文属于用户个人空间

paper_publications(paper_id)
-> 管理员把论文发布到全局论文库

Current Reading Model + retrieval_index_status
-> 论文是否可以进入 Agent Scope

底层 PDF、Reading Model 和 Qdrant Point 按 paper_id 共享,权限不写入 Qdrant。Java 先根据个人空间 与管理员发布记录得到可访问论文,再用统一可检索规则排除 PENDINGBUILDINGREBUILDINGFAILED 论文。

Research Turn 运行链

text
ChatHandler
-> ProductReadingConversationService
-> PythonResearchHarnessClient
-> POST /v1/research/stream
-> ResearchHarnessService
-> LiveResearchChatHarness
-> AgentsSdkHarnessRuntime
-> OpenAI Agents SDK Runner

这条链路每个用户回合都会重新创建。它拿到的 Corpus 已经被 Java 限定,Python Context、Session、 Tools 和 Model Client 只活到本次 Runner.run 结束。

一次研究回合怎样运行

1. Java 锁定产品范围

ChatHandler 接收 WebSocket 或 API 消息,确认用户、会话和当前可访问论文。论文选择、点击过的来源、 位置以及阅读动作以结构化字段进入 Effective Scope,不依赖模型从自然语言里猜。

2. 组装历史与 Research Memory

ProductReadingConversationService 读取近期会话、点击锚点和上一轮已经接受的引用。跨回合记忆只保留 进入过已接受答案的 Paper 与 Evidence;本轮搜索过但没有引用的候选不会自动成为长期事实。

3. 预留用量并建立流式请求

PythonResearchHarnessClient 在发请求前预留 Token 配额,将 SourceScope、History、Previous Evidence 和模型选项序列化到请求中,再调用 /v1/research/stream。Python 返回 NDJSON Progress, Java 负责取消、异常收口、用量结算以及向前端转发进度。

4. Python 只访问已授权论文

ResearchHarnessService 要求 user_idscope.paper_ids 非空,再通过 JavaCorpusGatewayReader 调用 Java Corpus API。Java 用 Qdrant 检索 Current Reading Model 候选并从 MySQL 精确读取。Python 没有“查全库再自行过滤”或整批加载 Reading Element 的步骤。

5. 创建本轮不可变输入

LiveResearchChatHarness 再次裁剪 Metadata Dataset,分配 run_id,可选地打开 EvalRecorder,然后创建 TurnExecutionInput。这个对象包含问题、文本历史、上一轮 Evidence、取消函数和进度监听器,交给 Runtime 后保持不变。

6. 装配一个请求级 Agent Runtime

AgentsSdkHarnessRuntime 为本轮创建:

  • 一个 ResearchRunContext
  • 一个请求级 ReadingCorpusTools
  • 一个 Model Adapter;
  • 一个 RequestBackedSession
  • 一组按当前 Corpus 生成的 Function Tools;
  • 一个 Agents SDK AgentRunner

Runtime 不持有长期会话。下一轮会重新装配请求级对象,避免授权集合、Evidence ID 或工具轨迹泄漏到 其他请求。

7. Runner 执行连续 Tool Loop

Agent 没有固定 Stage Machine,也没有预设“必须先规划再检索”的阶段序列。模型可以在一个连续循环里 改写 Query、换论文、补读位置、调用 Research Skill,或在提交被拒后继续修正。

Model Settings 要求每一步使用工具,reset_tool_choice=False 防止后续轮次退回自由文本。模型可以在 一次响应里提出多个 Function Call,但 max_function_tool_concurrency=1 会顺序执行它们。授权状态会被 前一个工具修改,串行执行可以保证后一个工具看到确定的 Paper、Location 和 Evidence 集合。

8. 只有接受的提交能结束 Run

普通工具结果都会回到同一个 Agent。submit_research_answer 也先作为工具执行,只有返回 accepted=true 时,tools_to_final_output 才让 SDK 结束 Run。未知 Evidence、错误引用、缺失 Coverage 都会返回结构化错误;同一批里还有其他工具调用时也不会结束 Run。模型继续在原来的 Runner 中修正。

9. Java 持久化产品结果

Python 把结构化 Outcome、Markdown、Citations、Usage、Research Memory 和可选 Trace 返回 Java。 Java 将接受的 Evidence 映射成 Product Reference,保存到会话,并在用户以后重开引用时重新检查权限。

Python Runtime 里各对象的职责

对象生命周期负责的内容
TurnExecutionInput单次 Run,只读Question、History、Authorized Dataset、Previous Evidence、Progress、Cancellation
ResearchRunContext单次 Run,可变Tool Trace、Model Usage、Tool Call Group,以及本轮 Corpus Tool State
ReadingCorpusTools单次 Run,可变Paper Disclosure、Location Disclosure、Java Corpus Gateway、Exact Read、Evidence Ledger
RequestBackedSession单次 SDK Run合并请求携带的文本历史和当前输入,不充当长期数据库
ResearchRunHooks每次 Model CallModel Call ID、Token、Latency、同批 Tool Call 关系
Model Adapter单次 RunChat Completions / Responses 协议、HTTP Capture、纯文本响应归一化
Agent单次 Run 配置Instructions、Model、Tools、Tool-use Behavior
Runner单次 Run 执行器继续执行 Model -> Tool -> Model,直到提交被接受或发生取消/技术失败

ResearchRunContext 主要维护三组逐步增长的状态:已公开 Paper、已公开 Location、已创建 Evidence。 工具调用前后的快照会进入 Eval Capture,因此离线分析能区分 Retriever 没暴露位置、Agent 没读、读了没引,以及引用只有 Heading 的情况。

harness_py 的工具与状态

harness_py 工具与状态变化图

工具按“研究权限如何逐步收窄”来设计:

阶段Tool前置条件状态变化
方法指导get_research_skill只记录本轮采用的研究范式,不改变 Corpus 权限
论文发现search_paper_candidatesJava-authorized Corpus返回权威 Metadata,并公开返回的 Paper
身份解析find_papers_by_identity结构化身份线索只有唯一 Match 会公开 Paper;Ambiguous 不授权
位置检索find_reading_locationsPaper 已公开返回不可引用 Preview,并公开 location_ref
准确读取read_locationsLocation 已公开返回原文 Span,创建 ev_... 并写入 Ledger
最终提交submit_research_answer已有足够状态,或明确选择 Clarify / Partial / Abstain校验 Outcome、Citation、Coverage;接受后结束 Run

Candidate Preview 的用途是导航。它会让模型知道下一步读哪里,但不能支撑论文方法、结果、性能或 局限性声明。read_locations 是当前唯一能创建可引用 Paper-content Evidence 的工具。

继续阅读 harness_py 的工具设计

三种状态不会混在一起

保存位置保存内容会不会进入下一轮模型输入
ConversationService 与 MySQL权限、会话、已接受 Reference、Research Memory会,以精简 History 和 Evidence Card 进入
ResearchRunContextDisclosed Paper、Location、Evidence、TraceRun 结束后释放;只有已引用 Evidence 被提升为长期记忆
EvalRecorder 输出events.jsonlresult.json,以及离线派生的 Golden Score、Judge / Human Label不驱动产品回答,只用于离线分析

这个区分避免了两类隐性状态:SDK Session 不会成为第二个会话数据库,Eval Dump 也不会反过来参与 线上回答。

当前 Corpus 与 Retrieval

Python 收到 Java 锁定的 scope.paper_ids 后,创建请求级 JavaCorpusGatewayReader。它只把本轮需要的 论文 Metadata 带进 Agent 状态,正文和索引不会整批复制到 Python。

工具调用经过三类 Java Corpus API:

text
论文发现 / 身份解析
-> MySQL 权威 Metadata

位置检索
-> Java BM25 Sparse Query
-> Qdrant `lexical_bm25_v1` 单次检索
-> 确定性的论文与 canonical Lead Coverage
-> Current Model / Index Contract 校验
-> MySQL 补全候选 Preview

准确读取
-> MySQL canonical location
-> Python Evidence Ledger

Qdrant Point 从 Current Reading Model 派生,每个可重新打开的 Page、Section、Table 或 Figure Location 对应一个 Point。Payload 只保存论文、模型版本、位置和路由字段;准确 span_text 仍从 MySQL 读取。

当前产品只使用 Qdrant Sparse BM25。Java 固定 Analyzer、Term ID、k1=1.2b=0.75 和全量重建时的 avgdl;Qdrant 使用 modifier=idf。产品路径不调用 Query Embedding,也没有 Dense、RRF 或内存 BM25 静默回退。内存 BM25 只保留为 Golden Fixture 与离线对照。

切换后的 69 条冻结查询达到 48/48 指定证据、24/24 完整 Case 和 0.48019 MRR;76 篇广查询 p50 为 132.1 ms。MiniMax 实际运行中有 47/48 份所需证据进入候选,只读取 29/48。测试边界、 切换验收和端到端归因见 Sparse Qdrant 切换与 MiniMax 证据缺口

Evidence 如何变成历史引用

一次引用会经过下面的身份链:

text
Reading Element / Location
-> read_locations exact span
-> deterministic Evidence ID
-> accepted [[evidence_id]] citation
-> Java Product Reference Mapping
-> historical permission check
-> reopen page / text / table / figure evidence

模型只生成结构化 Evidence Marker。最终数字编号和 Reference 列表由 Python 与 Java 根据已知 Evidence 渲染,模型不能手写一个 Sources 区域来绕过校验。

继续阅读

PaperLoom · Evidence-bounded Agentic RAG