Skip to content

Latest commit

 

History

History
879 lines (680 loc) · 34.7 KB

File metadata and controls

879 lines (680 loc) · 34.7 KB

ARH(Arhelper)系统架构

版本:v0.2(Phase 1-5 重构后) 日期:2026-05 作用对象:想参与 ARH 开发、定位 bug、或扩展通道 / 工具 / LLM 的工程师

这份文档描述的是 代码实际的样子,不是理想的样子。已知缺陷在相应章节明确标出。


1. 顶层分层

              ┌────────── 通道 ──────────┐
  user   →   │  CLI | Telegram | Feishu | WeChat | MCP Server  │
              └─────────────┬─────────────┘
                            │ UnifiedMessage
                            ▼
                ┌──────── Gateway ────────┐
                │  SessionStore           │   中心控制面
                │  Router + /command     │   命令短路 / 安全 / FastAPI
                │  SecurityGuard          │
                └─────────────┬───────────┘
                              │ AgentRequest
                              ▼
          ┌──────────── AgentRuntime ────────────┐
          │  dispatch_stream (唯一主引擎, ~850行) │
          │   ├─ PreDispatchHook  (plugin+NLU 短路)│
          │   ├─ PermissionMode   (default/plan/acceptEdits/yolo)│
          │   ├─ Memory Recall                    │
          │   ├─ Active Task 注入                  │
          │   ├─ ContextEngine.compact           │
          │   ├─ AgentLoop.run_stream            │─ LLM↔tool loop
          │   ├─ SubAgent  (spawn_subagent)       │
          │   ├─ L2 SessionReflector             │
          │   └─ L3 SkillExtractor                │
          │                                       │
          │  dispatch() = dispatch_stream + 聚合  │
          └─────────────┬───────────┬─────────────┘
                        │ LLM call  │ tool call
                        ▼           ▼
           ┌──────── LLM Layer ─────┐  ┌──── ToolRegistry ────┐
           │ ProviderRegistry       │  │ 31 个工具             │
           │ ProviderCache ⬅ Phase2 │  │ BaseTool + 自动发现   │
           │ OpenAI / Anthropic /   │  │ ApprovalGate (exec)   │
           │ Ollama / Fallback      │  │ PermissionMode 拦截   │
           │ RuntimeOverride        │  └──────────────────────┘
           │  → effective config    │
           └────────────────────────┘

       ┌────────── 数据/科研层(ARH 特色)──────────┐
       │  Crawler (6 平台)  Research Clients (7 个)  │
       │  SQLAlchemy + _session_scope (Phase 2)     │
       └─────────────────────────────────────────────┘

       ┌──────────── 存储 ────────────┐
       │  ~/.arh/config.yaml / .env    │
       │  data/ (tasks.db / memory/...)│
       │  arh/skills/ (builtin+learned)│
       └───────────────────────────────┘

核心指标(代码实测)

指标 数值
核心代码量 ~4,848 行(9 个核心文件)
工具数 31 个
Skill 数 11 builtin + 4 learned = 15
CLI 命令 50 个内置 + 15 个 skill 命令
通道数 4(CLI/Telegram/Feishu/WeChat)+ MCP server
LLM Provider 4 类(OpenAI 兼容覆盖 20+ 实际 provider)
测试 160+ 用例,含 Phase 0-5 重构守卫

2. 核心 Agent 循环(dispatch_stream)

2.1 唯一主引擎

AgentRuntime.dispatch_streamarh/runtime/agent_runtime.py:287-890, ~850 行)是唯一主引擎。Phase 1 合并了原先分裂的 dispatch / dispatch_stream 双路径。

async def dispatch_stream(self, request) -> AsyncIterator[StreamEvent]:
    # 1. 构建 RuntimeContext + 注入 contextvars
    #    (current_runtime, current_session, file_ops, permission_mode,
    #     artifact_tracker, abort_scope)

    # 2. PreDispatchHook 短路链(插件 / NLU 意图)
    #    命中 → yield TextComplete + StreamEnd,不调 LLM 直接返回

    # 3. L2 SessionReflector: on_session_start

    try:
        # 4. 获取 effective_llm_config(应用 RuntimeOverride)
        #    + ProviderCache.get_or_build → 复用 httpx Client

        # 5. 构建 system prompt
        #    - persona_prompt
        #    - tool schemas
        #    - memory recall (BM25 召回前 3 条)
        #    - active tasks 注入(session 未完成任务)
        #    - 上次 session handoff 摘要(L2 反思结果)
        #    - env_snapshot

        # 6. ContextEngine.compact(token 超阈值时 LLM 摘要压缩)

        # 7. AgentLoop.run_stream → 事件流
        #    ├─ TurnStart → LLMCallStart → 调 provider.chat
        #    ├─ /steer 注入(用户在 turn 间注入指导)
        #    ├─ tool_calls → PermissionMode 拦截 / ApprovalGate
        #    ├─ 工具执行(并发分级: parallel / exclusive)
        #    └─ 直到 TextComplete(is_final) 或 max_turns

        # 8. 幻觉检测(0 工具 + 含"已完成"关键词)

        # 9. L2: on_dispatch_end 累加 session 统计

        # 10. L3 SkillExtractor(>=5 工具 + 成功 + 非 override)
        #     后台 asyncio.create_task 不阻塞用户

        # 11. 保存对话历史

    except GeneratorExit:
        raise  # Ctrl+C / 切会话:正常传播
    except Exception as e:
        # Phase 3 顶层兜底:确保异常流出去
        yield ErrorEvent(message=f"内部错误: {e}")
        yield StreamEnd(success=False)

    finally:
        # 重置所有 contextvars

2.2 dispatch() 的退化

async def execute(self, request) -> AgentResponse:
    """纯聚合包装 —— dispatch_stream 的事件 → AgentResponse"""
    final_text, last_stream_end, amendments = "", None, []
    async for ev in self.dispatch_stream(request):
        if isinstance(ev, TextComplete) and ev.is_final:
            final_text = ev.text
        elif isinstance(ev, StreamEnd):
            last_stream_end = ev
        # ...
    return AgentResponse(reply=final_text + amendments, ...)

不变量(Phase 0 守卫测试锁定):dispatch(req).reply == drain(dispatch_stream(req)).final_text

2.3 AgentLoop 内循环

arh/core/agent_loop.py ~750 行。职责:LLM 调用 → tool_call 检测 → 工具执行 → 结果回传 → 循环,直到 TextComplete(is_final)max_turns

  • 工具并发:三级分类(read_only / parallel_safe / exclusive)
  • 重试error_classifier.classify_error 判 retryable → 指数退避 / 切 fallback model
  • 压缩compact_callback 在每轮前检查是否需要
  • Checkpoint:当前 session 有 in_progress task 时,每 2 轮保存快照到 task_store

3. 工具系统(31 个工具)

tools/base.py       ← BaseTool + ToolResult + ConcurrencyClass
      ↑
tools/registry.py   ← ToolRegistry,auto_discover() 用 pkgutil 扫 arh/tools/
      ↑
tools/*.py          ← 每个文件一个工具,import 时自动注册
      ↑
registry.to_api_schemas() → function-call schema 发给 LLM

工具分组

分组 工具 写类
文件 read_file / write_file / edit_file / glob / grep 后 3 是写类
执行 exec / process 都是写类,exec 过 ApprovalGate
网络 web_fetch(SSRF 防护)/ web_search
论文检索 search_paper / find_citations / find_references / find_similar / find_code / get_oa_pdf / export_citation / literature_review 否(智能 ID 解析)
研究分析 analyze_paper / recommend / check_deadlines
爬虫 crawl 写类
Agent 控制 task_manage / enter_plan_mode / exit_plan_mode / resume_task / spawn_subagent / run_skill 控制类

工具执行保护层(按优先级)

tool_call arrives
  │
  ├─ 1. PermissionMode 检查(plan 模式 + 写类工具 → auth_error)
  │
  ├─ 2. 并发分类 & 调度
  │     ├─ read_only + parallel_safe → asyncio.gather
  │     └─ exclusive → 串行
  │
  ├─ 3. ApprovalGate(仅 exec 命令)
  │     ├─ y: 本次允许
  │     ├─ a: 永久允许(写 allow_rules.json 指纹)
  │     └─ n: 拒绝
  │
  ├─ 4. 超时保护(每个 tool 声明 get_timeout())
  │     asyncio.wait_for + ToolResult.network_error 兜底
  │
  └─ 5. 结果大小截断(get_max_result_chars())
        避免返回给 LLM 的 tool_result 过长炸 context

4. 技能系统(Skills, 15 个)

arh/skills/
├── base.py                    # Skill dataclass (frontmatter + body)
├── loader.py                  # YAML frontmatter 解析 + 自动加载
├── builtin/                   # 11 个内置 skill
│   ├── arxiv.md               搜索/解析 arXiv → 结构化元数据 + BibTeX
│   ├── paper_deep_dive.md     论文深度解读
│   ├── topic_research.md      完整选题调研
│   ├── trend_analysis.md      领域趋势分析
│   ├── daily_digest.md        今日科研精选摘要
│   ├── writing_plans.md       多步任务规划生成器
│   ├── systematic_debugging.md  4 阶段根因分析
│   ├── test_driven_development.md  RED/GREEN/REFACTOR
│   ├── github_code_review.md
│   ├── codebase_inspection.md
│   └── ocr_documents.md
└── learned/                   # 4 个自动提取的 skill(L3 反思产物)
    ├── python-logging-integration/SKILL.md
    ├── flask-api-cicd-scaffold/SKILL.md
    ├── ml-model-comparison/SKILL.md
    └── check-and-generate-report/SKILL.md

Skill 三种触发方式

1. 显式 CLI 命令:每个 skill 自动注册为 /<skill-name>(kebab-case),由 commands.py:_register_skill_commands 完成:

"/arxiv GRPO reinforcement learning" prompt_template + 用户输入合并后走 dispatch_stream

2. LLM 主动调用 run_skill 工具:加载 SKILL.md 正文作为新轮的 system prompt。

3. SkillLoader 把描述塞进可选列表:LLM 在 "Skill: arxiv — 搜索/解析 arXiv..." 这一行后自主决定是否调。

L3 自我进化(SkillExtractor)

runtime/skill_extractor.py

  • 触发条件:depth=0 + StreamEnd.success + ≥5 tool_call + 非 system_prompt_override
  • 流程:后台 asyncio.create_task → LLM 生成 SKILL.md → CLI 弹审批 → 写入 learned/<slug>/SKILL.md热加载(不需重启 ARH)

5. 持久记忆(Memory)

三层架构(arh/interaction/):

memory.py              持久笔记(MEMORY.md 索引 + 主题文件)
                       跨会话保留,存到 ~/.arh/data/memory/

memory_index.py        BM25 索引,对所有主题文件建索引
                       → memory_recall.recall_relevant(query, top_k=3)

auto_memory.py         自动提取:每 N 轮(默认 10)让 LLM
                       从用户消息提取偏好 / 研究方向 / 教训

L2 SessionReflector

runtime/session_reflector.py

  • 触发/quit / 长静默 / compact 前钩子
  • 动作:让 LLM 生成 4 节摘要(Active / Resolved / Remaining / Insights)
  • 存储~/.arh/data/memory/sessions/{session_id}.md
  • 注入:下次同 user 新会话开始时,dispatch_stream 自动找最近摘要注入 system prompt

⚠ 已知缺陷(Phase 5 文档化)

AgentRuntime._memory_scope 字段是 no-op。SubAgent 设置 memory_scope="isolated" 在代码里完全没被读取 → 子 Agent 仍然召回全局 memory。

tests/test_phase5_subagent_profile.py::test_memory_scope_field_is_actually_consumed_by_dispatch@pytest.mark.xfail(strict=True) 守卫此缺陷,修复后测试自动变绿,强制维护者更新文档。


6. PreDispatchHook(Phase 1 新引入)

原 Pipeline 有 3 个 Stage(NLUStage / PluginDispatchStage / IntentHandleStage)做"命中短路"。Phase 1 删除 Pipeline 后,改为 arh/runtime/predispatch.py

PreDispatchHook = Callable[[UnifiedMessage, Any], Awaitable[Optional[str]]]

# 默认链
_default_hooks = [
    plugin_dispatch_hook,    # plugin_loader.dispatch_message 命中 → 返回 reply
    intent_handle_hook,      # NLU 置信度 ≥ 0.7 的 search/topic/analyze/trend 意图
                             # 直接调业务模块
]

# SubAgent 显式传 predispatch_hooks=[] 禁用短路
# (task 是结构化指令,不能被 NLU 误识别为 search)

dispatch_stream 在 LLM 调用前跑 hook 链。命中即 yield TextComplete + StreamEnd 直接返回,不调 LLM,不计 turns


7. PermissionMode(4 档权限)

arh/runtime/permission_mode.py(Phase 1 统一为唯一权限机制,删除了旧 PlanModeGate):

模式 行为
default 每次问 write 工具,用户 y/a/n
plan 阻止一切 write;LLM 必须 exit_plan_mode 申请批准才能干
accept_edits 文件写入自动通过;exec 仍问
yolo 全部免询问(用户对工作区完全信任)

切换路径

  • 用户命令:/mode [default|plan|accept_edits|yolo]
  • LLM 主动:调 enter_plan_mode() / exit_plan_mode() 工具
  • Session 级 20 分钟 TTL:plan 模式超时自动退出

ApprovalGate 指纹记忆allow_rules.py):用户按 a 永久允许后,相同命令指纹直接放行。指纹基于"命令链里真命令"(pip install && python -m pytest 会抽 pytest 做指纹)。


8. Gateway(多通道网关)

arh/gateway/gateway.py 是 Hub-Spoke 中心控制面:

class Gateway:
    async def _make_stream_handler(self, channel):
        async def handler(message):
            # 1. SecurityGuard 检查(auth_mode / rate_limit / bind_mode)
            # 2. SessionStore 解析 / 创建 session
            # 3. /command 短路:走 router._command_handlers 不进 runtime
            # 4. 派发到 runtime.dispatch_stream
            try:
                async for ev in self._default_stream_dispatch(request):
                    yield ev
            except Exception as e:
                # Phase 3 二级兜底
                yield ErrorEvent(...)
                yield StreamEnd(success=False)

支持的通道(4 个)

通道 依赖
CLI prompt_toolkit + rich
Telegram python-telegram-bot
Feishu lark-oapi
WeChat 公众号被动回复 + 客服消息

MCP Server(arh/server/mcp_server.py

FastMCP 对外暴露所有工具给 Claude Desktop / Cursor / Cline 等 MCP 客户端。SSE(HTTP)+ stdio 双模式。


9. LLM 层

4 类 Provider(覆盖 20+ 实际 provider)

arh/llm/providers/
├── openai_provider.py      OpenAI 兼容(DeepSeek/通义/OpenRouter/Kimi/GLM/...)
├── anthropic_provider.py   Anthropic Messages API
├── ollama_provider.py      本地 Ollama
└── fallback_provider.py    包装主 provider,失败按 cooldown 切备用

ProviderCache(Phase 2 新增)

背景:旧代码每次 dispatch 都 provider_cls(config.llm.model_dump()) 重建 provider,意味着:

  • 每次新建 httpx.AsyncClient(连接池失效)
  • Anthropic ephemeral prompt cache 跨轮无法命中
  • Provider 健康探测每轮重跑

arh/llm/provider_cache.py

_cache: Dict[CacheKey, BaseLLMProvider] = {}
# CacheKey = (provider_name, model, base_url, api_key)

def get_or_build(provider_name, llm_config, provider_cls) -> BaseLLMProvider:
    key = _make_key(provider_name, llm_config)
    if key in _cache:
        return _cache[key]  # 复用
    instance = provider_cls(llm_config)
    _cache[key] = instance
    return instance

def invalidate(provider_name=None) -> int:
    # /llm set / switch_model / app.stop 时清缓存

dispatch_stream / resume / reflect 所有 provider 实例化点都用 cached_provider(...)

Effective Config(Phase 3 修复关键 bug)

旧 bug/llm set model=xxx 存到 runtime_override.json,但 dispatch_stream 用 config.llm.model_dump()(未合并 override)→ 新 model 根本没被使用

修复arh/core/config.py

def get_effective_llm_config() -> Dict:
    """合并 RuntimeOverride 后的 LLM 配置"""
    base = get_config().llm.model_dump()
    return get_runtime_override().apply_to(base)

def get_effective_provider_name() -> str: ...

dispatch_stream 8 处原 config.llm.model_dump() 改为 get_effective_llm_config()。配合 Phase 2 的 provider_cache.invalidate(),形成完整"改配置立即生效"闭环:

/llm set model=gpt-4o
  → override 写入 runtime.json
  → provider_cache.invalidate()
  → 下次 dispatch: get_effective_llm_config() 拿新 model
  → get_or_build() cache miss → 新建 provider with gpt-4o

错误分类与 Fallback

arh/llm/error_classifier.pyErrorKind 枚举(auth / rate_limit / overloaded / server_error / context_overflow / network / invalid_response / other)

AgentLoop 内:

  • is_retryable(kind) → 指数退避 + jitter 重试
  • 重试用光 → _try_fallback_provider(kind)config.llm.fallback.fallback_models 挑下一个可用模型

10. 数据层(Phase 2 修复 DB session 泄漏)

Before

ConversationManagerPersonaManager 共 14 处 _get_session() 从不 close

session = self._get_session()
if session:
    try:
        session.add(conv)
        session.commit()
    except Exception as e:
        session.rollback()   # ← 没 close()

后果:SQLAlchemy session 累积、SQLite WAL writer lock 风险。

After(Phase 2)

_session_scope() context manager:

@contextmanager
def _session_scope(self):
    if not self._db:
        yield None; return
    session = self._db.get_session()
    try:
        yield session
    except Exception as e:
        session.rollback()
        logger.error(...)
        raise
    finally:
        session.close()   # 必定执行

14 处调用点全部改成 with self._session_scope() as session:

数据模型(arh/data/models.py

模型 用途
Paper 论文(含 doi / s2_id / openalex_id / citation_count / oa_status / venue / tldr / fields_of_study)
ConferenceDeadline 会议截止日期
Conversation 多对话管理(history JSON + persona_id)
InteractionRecord 交互日志
UserConfig 用户配置
Persona AI 人格

DatabaseManager 自动迁移

启动时对比 Base.metadataPRAGMA table_info,ALTER TABLE ADD COLUMN 补齐缺失列(仅 SQLite)。

⚠ 存量修复(Phase 2 附带)

.gitignore 规则 data/ 把整个 arh/data/ Python 包都忽略了 → 远程 main 完全没有 arh/data/ 目录的 7 个 Python 文件。任何人 clone 仓库都无法启动 ARH。Phase 2 commit 把 .gitignore 改为 /data/ 锚定根目录 + 把 arh/data/ 补进版本控制。


11. ServiceContainer(Phase 2 引入的 IoC 骨架)

目的:15 个全局单例(task_store / tool_registry / memory_manager / ...)分布在 30+ 文件、316+ 调用点。这带来测试隔离困难、SubAgent 真正隔离做不到、多 Profile 不可能。

arh/core/services.py

class ServiceContainer:
    def register(name, factory): ...        # lazy 注册
    def register_instance(name, obj): ...   # 直接登记实例
    def get(name): ...                      # 缓存 hit → factory → KeyError
    def replace(name, obj): ...             # 测试时换实现
    def reset(name=None): ...               # 清缓存
    def reset_all(): ...                    # 清 factory

_default_container = ServiceContainer()  # 进程级默认

def register_default_singletons(container):
    """包装现有 15 个全局单例到 container(兼容 shim)"""

ARHApp.__init__ 持有 self.services = get_default_container()init() 最后注册 19 个服务(包括 db / cache / runtime / gateway)。

当前状态

  • 容器本身就位:API 完整,守卫测试锁定
  • 包装层就位:从 container 取和 from x import singleton 返回同一实例
  • 调用点迁移未做:316+ 处调用仍用 from x import singleton 路径
  • 多 Profile 不可用:所有 ARHApp 共享同一个 default_container(Phase 5 xfail 守卫)

为什么不一把梭迁移

  • 功能层面零收益(两条路径返回同一实例)
  • 测试层面当前用 monkeypatch 也能活
  • 真实用户场景没有"同进程跑多 Profile"需求
  • 迁移涉及 316+ 文件改动,风险远大于收益

未来若真撞上痛点(例如:"我想在测试里替换整个 tool_registry"),再按子系统逐步迁移。


12. 数据/科研层(ARH 特色)

Crawler(arh/crawler/,6 个平台)

arxiv / huggingface / github / openreview / ccf_deadlines / dblp

每个 crawler 继承 BaseCrawler,支持增量 / 限速 / 关键词过滤。crawler/scheduler.py 用 APScheduler 做定时抓取。

Research Clients(arh/research/,7 个客户端)

模块 职责
api_client.py BaseAPIClient:令牌桶限速 / 指数退避 / follow_redirects
s2_client.py Semantic Scholar(论文搜索 / 引用 / 推荐;无 key 时 3s 限速)
openalex_client.py OpenAlex Works/Authors/Venues
crossref_client.py Crossref DOI + Content Negotiation
unpaywall_client.py OA 状态 + PDF 链接
pwc_client.py Papers With Code
dblp_client.py DBLP

业务层

  • recommender.py 多策略推荐
  • advisor.py topic_advisor
  • analyzer.py paper_analyzer
  • tracker.py trend_tracker
  • enricher.py 论文富化(DOI / citation / OA / venue / tldr)
  • citation_graph.py 引用图谱
  • search_orchestrator.py 多源搜索去重
  • keyword_translator.py 中文 → 英文学术关键词(内置词典)

13. CLI 层(Phase 4 拆分后)

Before(Phase 4 前)

arh/core/app.py 1919 行,其中 1500 行是 51 个 async def cmd_xxx 塞在 ARHApp._register_gateway_commands 一个类方法里。修命令要在巨文件里翻。

After(Phase 4)

arh/core/app.py              → 401 行(只剩启停 + 配置协调)
arh/gateway/cli/commands.py  → 1553 行(承接全部命令代码,按注释分 6 段)
arh/gateway/cli/adapter.py   → CLI 通道 adapter
arh/gateway/cli/completer.py → 自动补全

app._register_gateway_commands 简化为一行委派:

def _register_gateway_commands(self):
    if self.gateway is None or self.runtime is None:
        return
    from arh.gateway.cli.commands import register_all_commands
    register_all_commands(self, self.gateway.router, self.runtime)

命令分类(50 个内置 + 15 个 skill = 65 个)

类别 命令
Session /new /reset /clear /history /save /retry /undo /title /branch /fork /compress /snap /snapshot /stop /background /bg /btw /agents /tasks /queue /q /steer /status /resume
Info /profile /help /usage /insights /platforms /gateway /copy /debug
Config /config /model /provider /personality /verbose /yolo /mode /allow-rules /llm
Tools /tools /toolsets /skills /cron /reload /plugins /checkpoints /env
Skill 每个 skill 自动注册 /<name>(kebab-case):/arxiv /topic-research 等 15 个
Exit /exit

14. 错误传播与配置热重载(Phase 3)

错误传播三级兜底

1. AgentLoop 内 LLM 调用 → error_classifier 分类 → retry / fallback
2. dispatch_stream 顶层 except → yield ErrorEvent + StreamEnd(success=False)
3. Gateway._make_stream_handler 二级兜底 → 防止 async generator 打穿到 adapter

不变量:用户永远不会看到空白回复或未渲染的 traceback;所有错误都走事件流被 UI 渲染。

配置热重载闭环

/llm set model=xxx
  → RuntimeOverride.set("model", xxx) + save()
  → provider_cache.invalidate()
  │
下次 dispatch_stream
  → get_effective_llm_config() 合并 override
  → effective_cfg["model"] == xxx
  → cached_provider(name, effective_cfg, cls)  # cache miss
  → new OpenAIProvider with new model
  → httpx.AsyncClient 新建

Phase 2 做了 cache invalidate,Phase 3 补上了 effective config 应用。两者协同。


15. 运行时子系统一览

模块 职责
runtime/agent_runtime.py 核心,dispatch_stream 主引擎
runtime/predispatch.py PreDispatchHook 链(Phase 1)
runtime/subagent.py SubAgentManager,隔离度: session_id ✓ / conversation_id ✓ / memory_scope ✗(已知缺陷)
runtime/permission_mode.py 4 档权限模式(Phase 1 统一)
runtime/approval.py ApprovalGate,exec 命令人机审批
runtime/allow_rules.py 审批指纹持久化
runtime/fs_policy.py 文件系统黑白名单
runtime/checkpoint.py 任务级快照(每 2 轮自动)
runtime/task_store.py Todo / task_manage SQLite 存储
runtime/session_reflector.py L2:会话结束写 4 节摘要
runtime/skill_extractor.py L3:复杂任务成功后自动生成 SKILL.md
runtime/process_manager.py 后台子进程管理(/background
runtime/abort.py 中止协议(/stop
runtime/artifact_tracker.py 工具产物追踪
runtime/env_snapshot.py 环境探测(python / pkgs / git)
runtime/interaction_buffer.py /queue /steer 消息缓冲

16. 一次请求完整数据流

以 CLI 输入 > 帮我把这篇 arXiv 论文总结一下 https://arxiv.org/abs/2410.xxxxx 为例:

1. CLI adapter 封 UnifiedMessage(channel="cli", user_id, content)

2. Gateway._make_stream_handler:
     ├─ SecurityGuard.check
     ├─ SessionStore.resolve → SessionInfo
     ├─ 不是 /command → 派发 runtime.dispatch_stream

3. AgentRuntime.dispatch_stream:
     ├─ _build_context → RuntimeContext
     ├─ 注入 contextvars (runtime/session/file_ops/...)
     ├─ PreDispatchHook: NLU 识别为 chat 意图(非 SEARCH_PAPER),不短路
     ├─ L2 on_session_start
     │
     ├─ get_effective_llm_config() + cached_provider(...)
     │
     ├─ 构建 system_prompt:
     │   - tools schema
     │   - persona_prompt(conversation.persona_id)
     │   - memory_recall 召回 "论文阅读偏好"
     │   - active_tasks(如果有未完成)
     │   - 上次 session handoff 摘要
     │   - env_snapshot
     │
     ├─ ContextEngine.compact 检查(未触发)
     │
     ├─ AgentLoop.run_stream:
     │   ├─ Turn 1: LLM → tool_call: web_fetch(arXiv URL)
     │   │   → read_only 直接执行
     │   ├─ Turn 2: LLM → tool_call: analyze_paper(...)
     │   │   → 调 S2 + Crossref + Unpaywall 富化
     │   ├─ Turn 3: LLM → tool_call: write_file(report.md)
     │   │   → PermissionMode != plan → 走 ApprovalGate
     │   │   → 用户按 `a`:allow_rules 记下指纹
     │   │   → 执行成功,ArtifactTracker 记下产物
     │   └─ Turn 4: LLM → final TextComplete
     │
     ├─ 幻觉检测:4 个 tool_call,不触发
     │
     ├─ L2 on_dispatch_end (记 turns=4, tool_calls=3, tokens=...)
     │
     ├─ L3 SkillExtractor 评估:>=5 tool_calls? 否,跳过
     │
     └─ 对话历史保存(user + assistant)

4. gateway yield 的事件流:
   StreamStart → LLMCallStart → LLMCallEnd → ToolCallStart(web_fetch) →
   ToolCallComplete → TurnEnd → ... → TextComplete(is_final=True) → StreamEnd

5. CLI adapter 流式渲染(rich + renderer.py)

17. 重构轨迹(Phase 0-5)

Phase 核心改动 新测试
Phase 0 Characterization tests(dispatch_stream 12 用例) +13
Phase 1 双引擎合并 / 删 Pipeline 目录 / 删 PlanModeGate / 新增 PreDispatchHook +0
Phase 2 _session_scope / ProviderCache / ServiceContainer 骨架 + 修复 .gitignore 存量灾难arh/data/ 被误删 7 文件) +16
Phase 3 dispatch_stream 顶层异常兜底 / 修复 /llm set 不生效 bug / get_effective_llm_config +9
Phase 4 CLI 命令 1500 行从 app.py 拆到 commands.py(app.py 1919→401) +8
Phase 5 SubAgent 隔离性 + 多 Profile 守卫,文档化 2 个已知缺陷(memory_scope no-op / 多 ARHApp 共享 container) +12(10 pass + 2 xfail)

测试轨迹

时点 Passed xfailed
main 历史基线 192 0
Phase 1 205 0
Phase 2 221 0
Phase 3 230 0
Phase 4 238 0
Phase 5 248 2

全程 0 skip。2 个 xfail 都是 strict=True 模式——未来修好后强制要求维护者更新文档。


18. 已知缺陷清单(诚实披露)

🟡 中等优先级

1. AgentRuntime._memory_scope 字段是 no-op

  • SubAgent 设置 memory_scope="isolated"agent_runtime.py / memory_recall.py没有任何条件判定使用
  • 实际影响:SubAgent 仍然召回全局 memory,不是真正隔离。
  • 守卫:test_phase5_subagent_profile.py::test_memory_scope_field_is_actually_consumed_by_dispatch(xfail strict=True)
  • 修复方案:让 recall_relevant 接受 scope 参数,或 dispatch_stream 判定 self._memory_scope == 'isolated' 时跳过全局 recall。

2. 多 ARHApp 共享 default_container

  • get_default_container() 是进程级单例 → 两个 ARHApp 实例不可能真正独立。
  • 守卫:test_phase5_subagent_profile.py::test_two_arhapps_have_independent_services
  • 修复方案:ARHApp.__init__ 创建自己的 container,不用全局。但这要求把 316+ 处 from x import singleton 调用点都改为从 app.services.get('x') 取。业务痛点未到,暂不动。

🟢 低优先级(纯技术债)

3. 15 个全局单例 316+ 调用点未迁移

  • ServiceContainer 骨架已就位,但调用方仍 from arh.runtime.task_store import task_store
  • 实际影响:功能层面零影响;测试用 monkeypatch 也能凑合。
  • 不修的成本:未来某天真想做"同进程多 Profile"时,得 316 次全改。

4. 函数内延迟 import 约 347 处

  • 全仓 ^ from arh\. 有 347 处。
  • 实测:所有核心模块都能顶层 import 成功,零循环依赖。
  • 这 347 处全是合法懒加载(减少启动时间),不是循环依赖伪装。
  • 美观问题,无功能影响。

5. arh/gateway/cli/commands.py 1553 行单文件

  • Phase 4 从 1919 行拆成 389 + 1553 = 两个文件。按注释已分 6 段(Session / Info / Configuration / Tools / Skill / Exit)。
  • 进一步按类别拆 5 个文件的工作:风险收益比不高(命令之间互相引用 router._command_handlers 做 alias,拆分增加而不是减少复杂度)。

19. 可扩展点速查

新增工具

arh/tools/my_tool.py 继承 BaseTool
  ↓
registry.auto_discover() 启动时自动扫到
  ↓
LLM 看到 tool schema 可直接调用

新增 Skill

写 YAML frontmatter + Markdown 正文:
  arh/skills/builtin/my_skill.md
  或用 L3 SkillExtractor 自动提取:
  arh/skills/learned/<slug>/SKILL.md

新增通道

arh/gateway/my_channel/
  ├─ adapter.py  实现 ChannelAdapter
  └─ __init__.py  注册到 channel_registry

新增 LLM Provider

arh/llm/providers/my_provider.py 继承 BaseLLMProvider
  ↓
llm_registry.auto_discover()
  ↓
dispatch_stream 走 ProviderCache 缓存路径自动支持

新增爬虫

arh/crawler/<platform>/ 继承 BaseCrawler
  ↓
scheduler.register 挂到 APScheduler

新增 API 客户端

arh/research/my_client.py 继承 BaseAPIClient
  (令牌桶限速 / 指数退避 / 超时 / follow_redirects 自动继承)
  ↓
对应工具 arh/tools/xxx.py 里 get_my_client() 单例调用

20. 总结

ARH 是什么:面向科研场景的 Agent 框架。由 dispatch_stream 单引擎(Phase 1 合并双引擎后)驱动 LLM↔工具循环,配 PreDispatchHook 做 NLU/插件短路,带 4 档 PermissionMode + ApprovalGate 三重安全闸门,并有 L2 session reflect + L3 skill 自动提取的自我进化机制。

区别于其它 Agent 框架

  • 聚焦科研:6 平台爬虫 + 7 个学术 API 客户端 + 中文学术关键词翻译 + 论文 ID 智能解析 + Crossref Content Negotiation
  • 克制规模:31 工具 / 15 skill / 4 通道(不追求 hermes 那样的 28+ 工具 / 91 skill / 20+ 通道)
  • Plan Mode 硬绑定:写类工具必须先 task_manage + exit_plan_mode 过审批(不是靠 system prompt 提示)

一句话概括:dispatch_stream 是唯一引擎,其它都是装饰。


附录 A:关键文件定位

文件 行数 职责
arh/core/app.py 401 启停协调(Phase 4 瘦身)
arh/core/agent_loop.py 751 LLM↔tool 循环核心
arh/runtime/agent_runtime.py 1138 dispatch_stream 主引擎
arh/runtime/predispatch.py 193 Phase 1 新增:短路 hook
arh/runtime/subagent.py 159 SubAgent 管理
arh/runtime/permission_mode.py 167 4 档权限(Phase 1 统一)
arh/core/services.py 230 Phase 2 新增:IoC 骨架
arh/llm/provider_cache.py 112 Phase 2 新增:Provider 缓存
arh/gateway/gateway.py 311 Gateway 中心
arh/gateway/cli/commands.py 1553 Phase 4 新增:CLI 命令
tests/test_dispatch_characterization.py 704 Phase 0 守卫
tests/test_phase2_services.py 301 Phase 2 守卫
tests/test_phase3_error_config.py 230 Phase 3 守卫
tests/test_phase4_cli_split.py 158 Phase 4 守卫
tests/test_phase5_subagent_profile.py 461 Phase 5 守卫

附录 B:Phase 合并状态

Phase 分支 状态
Phase 1 refactor/phase-1-unify-dispatch ✅ 已合并 main
Phase 2 refactor/phase-2-services ✅ 已合并 main
Phase 3 refactor/phase-3-error-config ✅ 已合并 main
Phase 4 refactor/phase-4-cli-split ✅ 已合并 main
Phase 5 refactor/phase-5-subagent-profile-tests ⏳ PR 待 review