Add JSONL support, reorganize vault structure, and update design docs (#274)

* feat(config): add jsonl support and update LLM integration tests

- Added jsonl extension to supported extensions in chunked backend
- Refactored LLM integration tests to use async functions instead of nested runs
- Created helper functions _run_basic_chat, _run_with_tool, _run_structured_output, _run_structured_output_enum
- Implemented _run_all function to execute all test scenarios sequentially
- Updated main execution block to use asyncio.run with consolidated test runner
- Maintained all original test functionality while improving code structure

* feat(config): add dialog directory configuration and reorganize vault structure

- Add new dialog_dir field for dialog memory storage
- Reorder directory initialization sequence in application setup
- Simplify vault_dir description in configuration schema
- Update thread_pool_max_workers description to be more concise
- Move resource_dir definition earlier in the configuration schema
- Remove redundant text from digest_dir description

* docs(reme): update design documentation with layered memory architecture

- Replace quick test section with comprehensive layered memory structure
- Add detailed explanation of three-tier memory organization (resource, daily, digest)
- Document Obsidian-compatible Markdown format with YAML front matter
- Describe four types of wikilink syntax and semantic linking features
- Explain AST-aware semantic chunking for document parsing
- Detail self-evolving system with auto-resource, auto-memory, and auto-dream
- Document directory structure and lifecycle characteristics
- Add comprehensive table showing content nature, triggers, and examples
- Include semantic link extraction and knowledge graph formation processes
- Describe automated indexing and relationship building workflows

* docs(reme): update design documentation with simplified structure and clearer explanations

- Simplified directory structure overview with cleaner formatting
- Updated memory layering explanation with more concise descriptions
- Improved table layouts for better readability
- Clarified auto-resource, auto-memory, and auto-dream processes
- Streamlined indexing and search mechanism descriptions
- Enhanced component system documentation with clearer backend options
- Refined job list with more precise functional descriptions
- Modernized layout diagrams and process flows
- Consolidated repetitive content while maintaining comprehensive coverage

* docs(reme): add application scenario documentation for financial industry use case

- Document comprehensive example of ReMe usage in新能源 industry research
- Detail the week-long process of automatic knowledge graph construction
- Explain the auto-memory and auto-dream pipeline with concrete examples
- Describe the extract and integrate phases for creating wiki nodes
- Illustrate cross-file linking through relates_to and derived_from predicates
- Show progressive graph growth from daily sessions to complete ecosystem
- Demonstrate hybrid retrieval with vector and keyword search capabilities
- Provide detailed directory structure and file organization patterns
- Explain the three-phase workflow: ingestion, processing, and retrieval
- Document the financial analyst persona and their information management needs

* style(config): fix spacing in thread_pool_max_workers field definition

- Corrected spacing around description parameter in Field definition
- Simplified multi-line assertion to single line in LLM integration test
This commit is contained in:
jinliyl 2026-06-04 16:04:02 +08:00 committed by GitHub
parent 36a5512fc8
commit cee2c3e338
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 911 additions and 281 deletions

View file

@ -0,0 +1,55 @@
- 增加agent 的component
- 增加全局 时区time_zone全局作用
数据结构
- resource
- YYYYMMDD
- xxxx.html
- xxxx.txt
- xxxx.md
- dialog
- YYYYMMDD
- session_{session_id}.jsonl msg->dict格式
- daily
- YYYYMMDD.md 索引
- YYYYMMDD
- session_{session_id}.md 日志本
- resource_{resource_id}.md
- digest
- personal 个性化信息
- xxx.md
- procedural 程序化记忆
- xxx.md
- wiki 知识化记忆
- xxx.md
监控任务【后台】:
- 构建bm25+emb的index【5s】
- 目录daily+digest 的 所有md使用markdown的ast解析
- 可选jsonl要求保存的时候对工具结果截断使用rag方案解析
- daily md监控【1min】
- 暂无
- digest md监控【1min】
- 暂无
- resource 监控【间隔5min】
- 针对每一个文件生成一个hashid作为session_id
- 生成一个agent解读文件读前1MB内容防止上下文炸了
- 把抽取的内容写到daily/YYYYMMDD/resource_{resource_id}.md
- 调用推送工具:
- qwenpaw推送收件箱/agent
- cc可以直接推送agent @sen
hook任务
- auto-memory在上下文满/每隔多少轮/session_end
- qwenpaw直接传message session_id date直接append session_{session_id}.jsonl @jinli
- cc给出新的path对比我们保存的session_{session_id}.jsonl看到增量msg接着解析path的内容保存到session_{session_id}.jsonl @sen
- 注意保存的时候截断工具调用结构,防止太长
定时任务:
- auto-dream每天夜晚触发
- 记忆整理按照session/resource_id去做for循环整理把YYYYMMDD.md + YYYYMMDD/* 消化更新到 digest下的personal/procedural/wiki
- 记忆linkmarkdown之间linkdigest内部链接可以链接到外面。

192
docs4/old/reme_design.md Normal file
View file

@ -0,0 +1,192 @@
# 快速测试
```bash
# 终端 A启动服务
reme4 start
# 终端 B调用 version 验证服务可用
reme4 version
# 预期输出:✅ ReMe v{__version__}
```
# 基础Job
@jinli
入口:`reme4/reme.py::main()``parse_args(*sys.argv[1:])` 解析首个位置参数为 `action`,后续 `key=value` 解析为 kwargs支持
`service.port=8080` 的 dot notation自动剥离 `--` / `-` 前缀;值会做 bool / int / float / JSON 转换)。
调用模式:
- `start`:本地启动 `ReMe(Application)` 服务(不经过 client
- `find_reme`:本地探测正在运行的 reme不调用服务
- `list`:在 client 端拦截,不转发到服务端,直接返回 action 目录
- 其他 action通过 `call_server(action, **kwargs)``R.get(ComponentEnum.CLIENT, backend)` 实例化客户端并流式打印(任意未列出的
step register name 都按本规则透传)
通用可选参数 `backend:str=http`(取值 `http` / `mcp`,对应 `reme4/components/client/{http_client,mcp_client}.py`
`@R.register` 注册名);服务端默认 host/port 见 `reme4/constants.py`,可由 `start` 端通过 `service.host=` / `service.port=`
覆盖。
说明:📥 输入参数 📤 输出 ⭐ 必填 🎚️ 默认值 🛠️ 内部行为 📊 metadata
| 分类 | 指令 (register name) | 入口 | 参数 & 行为 |
|------------|--------------------------------------------------|-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 🚀 本地 | 🟢 `start` | `reme.py:30``ReMe(**kwargs).run_app()` | 📥 可选 `config=<name\|path>`(默认加载 `reme4/config/default.yaml``.yaml/.yml/.json` 都支持,含 `${ENV:-default}` 占位符)| 可选 `service.host=` / `service.port=` 等任意 dot-notation 覆盖 🛠️ 流程:`load_env()``resolve_app_config(**kwargs)` deep merge → `precheck_start(svc)``utils/service_utils.py:72`:目标 host:port 已有 reme → 打印 `reme already running ...` 直接返回;端口被其他进程占用 → stderr 提示 `port {port} occupied. Start on another port: reme4 start service.port=<other_port>``sys.exit(1)`)→ 启动服务 |
| 🚀 本地 | 🧭 `find_reme` | `reme.py:36``utils/service_utils.py:89` | 📥 无 📤 发现服务则 stdout 打印 `HOST={host} PORT={port} PID={pid or 'unknown'}`;未发现则 stderr 提示 `reme not started. Try: reme start``sys.exit(1)` 🛠️ 流程:先探 `REME_DEFAULT_HOST:REME_DEFAULT_PORT``health_check` 命中算 `reme`),再 `pgrep -af "reme.* start"` 扫描其他端口 |
| 🛰️ 客户端 | 📜 `list` | `components/client/base_client.py:36` | 📥 无 📤 服务端可用 action 目录JSON`indent=2 ensure_ascii=False` 🛠️ 在 `BaseClient.__call__` 中拦截,不进入 `_execute`,直接调用 `list_actions()`HTTP/MCP backend 各自实现) |
| 🌐 通用 step | 🆘 `help` (`help_step`) | `call_server("help")` | 📥 无 📤 `answer` 一行一个 job`🛠️ \`{name}\` — {description} 📥 {params}`,参数渲染为 `name:type*`(必填) / `name:type={default}` / `name:type` 📊 `metadata.job_count` 🛠️ 自动跳过名为 `help` 的 job |
| 🌐 通用 step | 🩺 `health_check` (`health_check_step`) | `call_server("health_check")` | 📥 无 📤 `answer = "✅/❌ ReMe v{version} - healthy/unhealthy"` 📊 `metadata.health = {version, healthy, components}` 🧩 覆盖组件:`embedding_model`(🟢 is_started/is_healthy/model_name/dimensions/cache_size/memory) · `file_graph`(🕸️ n_nodes/n_edges/n_virtual\|n_pending/memory) · `file_store`(📦 n_chunks/n_chunks_with_embedding/memory) · `file_watcher`(👀 background_running/watch_paths) · `keyword_index`(🔤 n_docs/vocab_size/memory) 🛠️ deep sizeof含 numpy.nbytes未启动 / 后台未跑 / embedding 不健康 → ❌ |
| 🌐 通用 step | 🏷️ `version` (`version_step`) | `call_server("version")` | 📥 无 📤 `answer = reme4.__version__` 📊 `metadata.version` |
| 🌐 通用 step | 🔄 `reindex` (`reindex_step`) | `call_server("reindex")` | 📥 无 📤 `answer = "🔄 Reindexed {added} file(s)"` 📊 `metadata.counts = {added, ...}` 🛠️ 流程:`file_watcher.close()``file_store.clear()``file_watcher.update_store()``file_watcher.start()`finally 保证重启) |
| 🔎 search | 🔍 `search` (`search_step`) | `call_server("search", query=…, …)` | 📥 `query:str` 🎚️ `limit:int=5`(>0) 🎚️ `min_score:float=0.0` ⚖️ `vector_weight:float=0.7` ∈[0,1]keyword 权 = 1-vw 🔀 `candidate_multiplier:float=3.0`candidates = min(200, limit×mult) 🔗 `expand_links:bool=True` 🔢 `max_links_per_direction:int=10` 🎚️ `search_filter:dict={}` 📤 `answer` 每命中一行 `path:start-end [score=… vector=… keyword=…] text` + 缩进的 `→ outlinks (n)` / `← inlinks (n)` + `via predicate=… anchor=#…` 📊 `metadata.results` / `metadata.link_expansion` / `metadata.counts={vector,keyword,returned,hybrid}` 🛠️ 并行 `vector_search` + `keyword_search` → RRF 融合K=60按 chunk.id 合并)→ `min_score` 过滤 → `limit` 截断 → 邻居 meta 注入 |
| 🧪 demo | 🪄 `demo_echo` (`demo_echo_step1` + `step2`) | `call_server("demo_echo", query=…, min_score=…)` | 📥 `query:str=""` 🎚️ `min_score:float=0.5` 🛠️ step1`processed_query = query.strip().lower()``adjusted_min_score = min_score * 0.9`,写回 context 📤 step2`answer = "echo: {processed_query} (min_score={adjusted_min_score})"` 📊 `metadata = {step, query, min_score, processed_query, adjusted_min_score}` |
| 🌊 demo | 🌊 `stream_demo` (`stream_demo_step1` + `step2`) | `call_server("stream_demo", query=…, repeat=…, interval=…)` | 📥 `query:str=""` 🎚️ `repeat:int=10` 🎚️ `interval:float=0.1`(秒/字符)| 🛠️ step1`stream_text = query * repeat` 写回 context 📤 step2按字符 `add_stream_string(ch, ChunkEnum.CONTENT)` 流式输出,`asyncio.sleep(interval)` 节流 |
| 📂 crud | 📖 `read` (`read_step`) | `call_server("read", path=…, …)` | 📥 `path:str` ⭐(**完整相对路径**,相对于 vault绝对路径会被拒绝`.md` 后缀拒绝)| 🎚️ `start_line:int=null`1-based, 含端点)| 🎚️ `end_line:int=null`1-based, 含端点)| 🎚️ `max_bytes:int=51200`(截断阈值)| 📤 `answer = 选中的行内容`,超过 `max_bytes` 时附加 `--- TRUNCATED ---` 续读指引(`start_line=…` 📊 `metadata.path` / `metadata.total_lines`(出错路径才会附带)| 🛠️ 流程:`BaseStep.resolve_path(raw, require_md=True)``aiofiles.os.stat``read_file_safe`utf-8-sig BOM 容忍、UnicodeDecodeError fallback `errors=ignore`)→ `split("\n")` 切片 `[s-1:e]``truncate_text_output` 按字节截断保行 |
使用示例:
```bash
# 启动(默认 default.yaml
reme4 start
# 指定 config 与服务端口
reme4 start config=paw.yaml service.port=8181
# 查找在跑的 reme
reme4 find_reme
# HOST=127.0.0.1 PORT=8000 PID=12345
# 列出所有可用 actionclient 端处理,不转服务端)
reme4 list
# 转发到服务端的 step所有 key=value 透传为 step kwargs
reme4 help
reme4 health_check
reme4 version
reme4 reindex
reme4 search query="latency 问题" limit=10 min_score=0.2 vector_weight=0.6
# 读取 vault 下的 markdown完整相对路径无后缀自动补 .md可按行切片或限制字节
reme4 read path=Templates/Recipe.md
reme4 read path=Notes start_line=1 end_line=20
reme4 read path=Big.md max_bytes=4096
# 通过 MCP backend 调用
reme4 search query="..." backend=mcp
```
@sen
| file | upload/download/move/delete/stat/list | 文件操作CRUD |
| property | read/update/delete | frontmatter CRUD | |
| graph | traverse/retarget | path="My Note" directtion=forward/backward depth=1 predicat=xxx |
@wangce
| crud | write | path="New Note" name="xxx" description="xxx" metadata={}, content="# Hello" (4 字段都必填frontmatter 只写 name/description) |
| crud | read | path="Templates/Recipe.md" |
| crud | edit | path="Templates/Recipe.md" old="xxx" new="xxx" |
| crud | append | path="My Note" content="New line" |
| crud | delete | path="My Note
| daily_crud | daily_xxx | 与 crud 参数保持一致 |
- daily_resolve name=xxxx (符合一定规范 win下要求)
- daily_list date=xxxx 返回path
- daily_index
frontmatter read path
frontmatter update path metadata={}
frontmatter delete path keys=[]
delete path
download path=xxx内部相对路径download_path=(外部绝对路径,可选)
upload path=xxx外部绝对路径description="xxx" metadata=xxx 返回内部相对路径 加metadata
stat path
list path
mv path=xxx new_path=xxx
traverse path=xxx direction=xxx depth=xxx
# 日记类型
| 类型 | 路径 | 说明 |
|-----------|-----------------------------------------------|-----------------------------|
| daily | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | 按日期归档的原始信息记录 |
| topic | topic/{topic:-personal(agent)}/{xxxx}.md | 按主题聚类的二次加工内容 |
| proactive | todo | 基于 daily / topic 思考后主动推送的消息 |
# 生成Job
| 任务 | 输入 | 输出 | 触发时机 | 说明 |
|-------------------------|---------------|-----------------------------------------------|-----------------------------|------------------------------------------------------|
| 日记summary @sen @wangce | msg | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | freq (every_n_turn、compact) | 把 msg 的信息写入 daily 目录 |
| 主题dream + 生成链接 @sen | daily/xxx | knowledge/xxx | /dream | 把 daily 目录的内容按主题聚类合并到 topic 目录, 主动在文档中建立 [[link]] 关联 |
| 主动proactive @wangce | daily / topic | proactive_query | pre_query | 思考 daily / topic 信息,主动决定推送给用户的消息 |
2. file_parser
a. 抽象基类 parse: @jinli
. 输入是path相对路径
ⅱ. 输出是FileMetadata & list[FileChunks] & list[FileEdge]
b. default parser 兼容老方案 @jinli
. 带overlap的chunking策略 不输出FileEdge
c. markdown parser @sen
. 根据markdown ast做chunk不需要overlap
ⅱ. 增加一个索引的chunk chunk_type @锦鲤 file_chunk_type content/index
ⅲ. 增加link的正则解析predicate:: [[path#anchor]]
3. file_store @sen
a. 抽象存储:
. filenode = file + path + st_mtime + metadata + list[FileEdge]
ⅱ. graph=dict[str, filenode] 内存+json
ⅲ. list[FileChunk] 存db
b. 抽象基类
. graphfellow dict的操作 update/get/set
ⅱ. chunks dict[str, list[chunk]]
1. delete_chunks_by_path
2. update_chunks_by_path
3. list_chunks_by_path
4. vector_search/keyword_search
ⅲ. 手写一个bm25检索
ⅳ. 【核心】检索机制 vector bm25 graph 如何进行融合
4. file_watcher @jinli
a. 抽象基类
. on_start:
1. file_store 的start 在前加载graphfile_watcher在后递归扫描目录
a. 通过ms_time对比graphon_change 进行改动
ⅱ. on_change:
1. 更新/增加:
a. delete_chunks_by_path 更新数据库
b. upate_chunks_by_path 更新数据库
c. 更新graph
2. 删除
a. delete_chunks_by_path 更新数据库
MemorySchema
1. markdown文件结构 @sen
a. formatter
. name
ⅱ. desc
2. memory文件结构目录
a. MEMORY.md
b. msg/files -> daily/YYYYMMDD/YYYYMMDD.md + xxxx.md
. YYYYMMDD.md
1. xxx -> xxxx.md
2. xxx -> xxxd.md
ⅱ.
c. daily -> topic/topic_l1/topic_l1.md + xxx.md + topic_l2
d. proactive
steps:
1. 治理(算法+LLM
a. 节点关联P0现有的链接做补充挖掘新的LLM的link
. /Users/yuli/workspace/ReMe/reme2/component/edge_extractor/llm_edge_extractor.py
ⅱ. 移动到steps
b. 节点整合/节点拆分/节点归档
c. 健康度检查
2. retrieve 调用store的检索
3. 原子stepsreme edit
4. 组合steps总结
a. - freq (every_n_turn、compact) -> daily_summarizer
b. topic (/dream ) -> topic_summarizer(daily_xx -> topic_xx)
c. proactive -> proactive_summarizer(personal_xxx -> proactive_query - pre_query

View file

@ -1,192 +1,385 @@
# 快速测试
# ReMe 设计文档
```bash
# 终端 A启动服务
reme4 start
## 整体定位
# 终端 B调用 version 验证服务可用
reme4 version
# 预期输出:✅ ReMe v{__version__}
> 一句话总结:**自进化的个人知识库**——你只管往里扔东西和对话,它自己长成一张知识图谱。
## 特性1记忆分层
记忆按"原始 → 浅加工 → 深加工"三层组织:
### 1.1 目录结构
```
- resource/【原始素材】 # 外部渠道摄入 / 手动放入
- YYYY-MM-DD/ # 按日期归档
- session_{id}.jsonl # 对话原始记录
- {channel}_{xxxx}.html # 网页抓取、邮件等
- {channel}_{xxxx}.md # Markdown 资料
- daily/【日记,浅加工】 # auto-memory 自动写入
- YYYY-MM-DD.md # 当天索引页,汇总所有事件
- YYYY-MM-DD/
- session_{id}.md # 按 session 拆分的日志
- resource_{id}.md # 对素材的加工笔记
- digest/【深加工】 # auto-dream 持续打磨
- personal/ # 用户偏好、习惯、身份
- procedure/ # 方法论、步骤、工作流
- wiki/ # 通用知识、决策先例
```
# 基础Job
### 1.2 分层详解
@jinli
| 目录 | 存什么 | 谁写入 | 举例 |
|---------------------|----------------|-------------|-----------------------------------|
| `resource/` | 原始文件(研报、网页、邮件) | upload / 手动 | PDF 研报、对话 JSONL |
| `daily/` | 每天的事件记录 | auto-memory | "调试登录 CSS"、"与 Alice 聚餐" |
| `digest/procedure/` | 方法论、步骤 | auto-dream | "webpack 编译卡死排查路径" |
| `digest/personal/` | 用户画像、偏好 | auto-dream | "用户不爱写注释"、"用户喜欢 pnpm" |
| `digest/wiki/` | 通用知识、决策先例 | auto-dream | "光伏产业链"、"React Server Components" |
入口:`reme4/reme.py::main()``parse_args(*sys.argv[1:])` 解析首个位置参数为 `action`,后续 `key=value` 解析为 kwargs支持
`service.port=8080` 的 dot notation自动剥离 `--` / `-` 前缀;值会做 bool / int / float / JSON 转换)。
`resource/``daily/` 是只增不删的流水账;`digest/` 下三个桶是反复消费的精华层,各桶有独立的整合 prompt。
调用模式:
## 特性2Obsidian 兼容的 Markdown 格式
- `start`:本地启动 `ReMe(Application)` 服务(不经过 client
- `find_reme`:本地探测正在运行的 reme不调用服务
- `list`:在 client 端拦截,不转发到服务端,直接返回 action 目录
- 其他 action通过 `call_server(action, **kwargs)``R.get(ComponentEnum.CLIENT, backend)` 实例化客户端并流式打印(任意未列出的
step register name 都按本规则透传)
所有笔记都是标准 Markdown + Obsidian 语法,可以直接用 Obsidian 打开浏览:
通用可选参数 `backend:str=http`(取值 `http` / `mcp`,对应 `reme4/components/client/{http_client,mcp_client}.py`
`@R.register` 注册名);服务端默认 host/port 见 `reme4/constants.py`,可由 `start` 端通过 `service.host=` / `service.port=`
覆盖。
说明:📥 输入参数 📤 输出 ⭐ 必填 🎚️ 默认值 🛠️ 内部行为 📊 metadata
| 分类 | 指令 (register name) | 入口 | 参数 & 行为 |
|------------|--------------------------------------------------|-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 🚀 本地 | 🟢 `start` | `reme.py:30``ReMe(**kwargs).run_app()` | 📥 可选 `config=<name\|path>`(默认加载 `reme4/config/default.yaml``.yaml/.yml/.json` 都支持,含 `${ENV:-default}` 占位符)| 可选 `service.host=` / `service.port=` 等任意 dot-notation 覆盖 🛠️ 流程:`load_env()``resolve_app_config(**kwargs)` deep merge → `precheck_start(svc)``utils/service_utils.py:72`:目标 host:port 已有 reme → 打印 `reme already running ...` 直接返回;端口被其他进程占用 → stderr 提示 `port {port} occupied. Start on another port: reme4 start service.port=<other_port>``sys.exit(1)`)→ 启动服务 |
| 🚀 本地 | 🧭 `find_reme` | `reme.py:36``utils/service_utils.py:89` | 📥 无 📤 发现服务则 stdout 打印 `HOST={host} PORT={port} PID={pid or 'unknown'}`;未发现则 stderr 提示 `reme not started. Try: reme start``sys.exit(1)` 🛠️ 流程:先探 `REME_DEFAULT_HOST:REME_DEFAULT_PORT``health_check` 命中算 `reme`),再 `pgrep -af "reme.* start"` 扫描其他端口 |
| 🛰️ 客户端 | 📜 `list` | `components/client/base_client.py:36` | 📥 无 📤 服务端可用 action 目录JSON`indent=2 ensure_ascii=False` 🛠️ 在 `BaseClient.__call__` 中拦截,不进入 `_execute`,直接调用 `list_actions()`HTTP/MCP backend 各自实现) |
| 🌐 通用 step | 🆘 `help` (`help_step`) | `call_server("help")` | 📥 无 📤 `answer` 一行一个 job`🛠️ \`{name}\` — {description} 📥 {params}`,参数渲染为 `name:type*`(必填) / `name:type={default}` / `name:type` 📊 `metadata.job_count` 🛠️ 自动跳过名为 `help` 的 job |
| 🌐 通用 step | 🩺 `health_check` (`health_check_step`) | `call_server("health_check")` | 📥 无 📤 `answer = "✅/❌ ReMe v{version} - healthy/unhealthy"` 📊 `metadata.health = {version, healthy, components}` 🧩 覆盖组件:`embedding_model`(🟢 is_started/is_healthy/model_name/dimensions/cache_size/memory) · `file_graph`(🕸️ n_nodes/n_edges/n_virtual\|n_pending/memory) · `file_store`(📦 n_chunks/n_chunks_with_embedding/memory) · `file_watcher`(👀 background_running/watch_paths) · `keyword_index`(🔤 n_docs/vocab_size/memory) 🛠️ deep sizeof含 numpy.nbytes未启动 / 后台未跑 / embedding 不健康 → ❌ |
| 🌐 通用 step | 🏷️ `version` (`version_step`) | `call_server("version")` | 📥 无 📤 `answer = reme4.__version__` 📊 `metadata.version` |
| 🌐 通用 step | 🔄 `reindex` (`reindex_step`) | `call_server("reindex")` | 📥 无 📤 `answer = "🔄 Reindexed {added} file(s)"` 📊 `metadata.counts = {added, ...}` 🛠️ 流程:`file_watcher.close()``file_store.clear()``file_watcher.update_store()``file_watcher.start()`finally 保证重启) |
| 🔎 search | 🔍 `search` (`search_step`) | `call_server("search", query=…, …)` | 📥 `query:str` 🎚️ `limit:int=5`(>0) 🎚️ `min_score:float=0.0` ⚖️ `vector_weight:float=0.7` ∈[0,1]keyword 权 = 1-vw 🔀 `candidate_multiplier:float=3.0`candidates = min(200, limit×mult) 🔗 `expand_links:bool=True` 🔢 `max_links_per_direction:int=10` 🎚️ `search_filter:dict={}` 📤 `answer` 每命中一行 `path:start-end [score=… vector=… keyword=…] text` + 缩进的 `→ outlinks (n)` / `← inlinks (n)` + `via predicate=… anchor=#…` 📊 `metadata.results` / `metadata.link_expansion` / `metadata.counts={vector,keyword,returned,hybrid}` 🛠️ 并行 `vector_search` + `keyword_search` → RRF 融合K=60按 chunk.id 合并)→ `min_score` 过滤 → `limit` 截断 → 邻居 meta 注入 |
| 🧪 demo | 🪄 `demo_echo` (`demo_echo_step1` + `step2`) | `call_server("demo_echo", query=…, min_score=…)` | 📥 `query:str=""` 🎚️ `min_score:float=0.5` 🛠️ step1`processed_query = query.strip().lower()``adjusted_min_score = min_score * 0.9`,写回 context 📤 step2`answer = "echo: {processed_query} (min_score={adjusted_min_score})"` 📊 `metadata = {step, query, min_score, processed_query, adjusted_min_score}` |
| 🌊 demo | 🌊 `stream_demo` (`stream_demo_step1` + `step2`) | `call_server("stream_demo", query=…, repeat=…, interval=…)` | 📥 `query:str=""` 🎚️ `repeat:int=10` 🎚️ `interval:float=0.1`(秒/字符)| 🛠️ step1`stream_text = query * repeat` 写回 context 📤 step2按字符 `add_stream_string(ch, ChunkEnum.CONTENT)` 流式输出,`asyncio.sleep(interval)` 节流 |
| 📂 crud | 📖 `read` (`read_step`) | `call_server("read", path=…, …)` | 📥 `path:str` ⭐(**完整相对路径**,相对于 vault绝对路径会被拒绝`.md` 后缀拒绝)| 🎚️ `start_line:int=null`1-based, 含端点)| 🎚️ `end_line:int=null`1-based, 含端点)| 🎚️ `max_bytes:int=51200`(截断阈值)| 📤 `answer = 选中的行内容`,超过 `max_bytes` 时附加 `--- TRUNCATED ---` 续读指引(`start_line=…` 📊 `metadata.path` / `metadata.total_lines`(出错路径才会附带)| 🛠️ 流程:`BaseStep.resolve_path(raw, require_md=True)``aiofiles.os.stat``read_file_safe`utf-8-sig BOM 容忍、UnicodeDecodeError fallback `errors=ignore`)→ `split("\n")` 切片 `[s-1:e]``truncate_text_output` 按字节截断保行 |
使用示例:
```bash
# 启动(默认 default.yaml
reme4 start
# 指定 config 与服务端口
reme4 start config=paw.yaml service.port=8181
# 查找在跑的 reme
reme4 find_reme
# HOST=127.0.0.1 PORT=8000 PID=12345
# 列出所有可用 actionclient 端处理,不转服务端)
reme4 list
# 转发到服务端的 step所有 key=value 透传为 step kwargs
reme4 help
reme4 health_check
reme4 version
reme4 reindex
reme4 search query="latency 问题" limit=10 min_score=0.2 vector_weight=0.6
# 读取 vault 下的 markdown完整相对路径无后缀自动补 .md可按行切片或限制字节
reme4 read path=Templates/Recipe.md
reme4 read path=Notes start_line=1 end_line=20
reme4 read path=Big.md max_bytes=4096
# 通过 MCP backend 调用
reme4 search query="..." backend=mcp
```
┌─────────────────────────────────────────────────────────────┐
│ 一个 .md 文件的完整结构 │
├─────────────────────────────────────────────────────────────┤
│ --- │
│ name: 宁德时代 ← YAML front matter │
│ description: 全球动力电池龙头 │
│ tags: [新能源, 电池] │
│ --- │
├─────────────────────────────────────────────────────────────┤
│ 所属行业:: [[新能源]] ← 语义化链接Dataview
│ 竞争对手:: [[比亚迪]] │
│ │
│ # 基本面 ← Markdown 正文 │
│ 全球动力电池出货量第一,核心技术为 │
│ [[CTP]] 和 [[钠离子电池]]…… ← 标准 wikilink │
│ │
│ 参考 ![[2026Q1调研纪要]] ← 嵌入引用 │
├─────────────────────────────────────────────────────────────┤
│ ↓ AST 语义分块 ↓ │
│ chunk 1: [标题骨架] + 正文片段 │
│ chunk 2: [标题骨架] + 正文片段 │
└─────────────────────────────────────────────────────────────┘
```
@sen
| file | upload/download/move/delete/stat/list | 文件操作CRUD |
| property | read/update/delete | frontmatter CRUD | |
| graph | traverse/retarget | path="My Note" directtion=forward/backward depth=1 predicat=xxx |
### 2.1 YAML front matter
@wangce
| crud | write | path="New Note" name="xxx" description="xxx" metadata={}, content="# Hello" (4 字段都必填frontmatter 只写 name/description) |
| crud | read | path="Templates/Recipe.md" |
| crud | edit | path="Templates/Recipe.md" old="xxx" new="xxx" |
| crud | append | path="My Note" content="New line" |
每个笔记头部的元数据:
| crud | delete | path="My Note
| daily_crud | daily_xxx | 与 crud 参数保持一致 |
```markdown
---
name: 光伏产业链研究
description: 从硅料到组件的全链条梳理
tags: [新能源, 光伏, 产业链]
---
```
- daily_resolve name=xxxx (符合一定规范 win下要求)
- daily_list date=xxxx 返回path
- daily_index
`name` / `description` 是约定字段,其余键值对全部保留,不会丢弃任何自定义字段。
frontmatter read path
frontmatter update path metadata={}
frontmatter delete path keys=[]
### 2.2 四种 wikilink 写法
delete path
download path=xxx内部相对路径download_path=(外部绝对路径,可选)
upload path=xxx外部绝对路径description="xxx" metadata=xxx 返回内部相对路径 加metadata
stat path
list path
mv path=xxx new_path=xxx
| 写法 | 示例 | 语义 |
|------|----------------|----------|
| 标准链接 | `[[光伏产业链]]` | 指向目标文件 |
| 锚点链接 | `[[钴#应用]]` | 指向特定章节 |
| 别名链接 | `[[宁德时代\|宁德]]` | 自定义显示文本 |
| 嵌入引用 | `![[钴]]` | 内联嵌入目标内容 |
traverse path=xxx direction=xxx depth=xxx
### 2.3 语义化链接Dataview 风格)
# 日记类型
普通 wikilink 只说"A 提到了 B",语义化链接还能表达"A 和 B 是什么关系"
| 类型 | 路径 | 说明 |
|-----------|-----------------------------------------------|-----------------------------|
| daily | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | 按日期归档的原始信息记录 |
| topic | topic/{topic:-personal(agent)}/{xxxx}.md | 按主题聚类的二次加工内容 |
| proactive | todo | 基于 daily / topic 思考后主动推送的消息 |
```markdown
所属行业:: [[新能源]] ← 行级属性(独占一行)
总部:: [[宁德]]
[竞争对手:: [[比亚迪]]] ← 内联属性(嵌入正文中)
```
# 生成Job
`WikilinkHandler` 是全系统唯一的 wikilink 解析入口,确保 parser、graph、search 各层规则一致。
| 任务 | 输入 | 输出 | 触发时机 | 说明 |
|-------------------------|---------------|-----------------------------------------------|-----------------------------|------------------------------------------------------|
| 日记summary @sen @wangce | msg | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | freq (every_n_turn、compact) | 把 msg 的信息写入 daily 目录 |
| 主题dream + 生成链接 @sen | daily/xxx | knowledge/xxx | /dream | 把 daily 目录的内容按主题聚类合并到 topic 目录, 主动在文档中建立 [[link]] 关联 |
| 主动proactive @wangce | daily / topic | proactive_query | pre_query | 思考 daily / topic 信息,主动决定推送给用户的消息 |
### 2.4 AST 感知的语义分块
2. file_parser
a. 抽象基类 parse: @jinli
. 输入是path相对路径
ⅱ. 输出是FileMetadata & list[FileChunks] & list[FileEdge]
b. default parser 兼容老方案 @jinli
. 带overlap的chunking策略 不输出FileEdge
c. markdown parser @sen
. 根据markdown ast做chunk不需要overlap
ⅱ. 增加一个索引的chunk chunk_type @锦鲤 file_chunk_type content/index
ⅲ. 增加link的正则解析predicate:: [[path#anchor]]
3. file_store @sen
a. 抽象存储:
. filenode = file + path + st_mtime + metadata + list[FileEdge]
ⅱ. graph=dict[str, filenode] 内存+json
ⅲ. list[FileChunk] 存db
b. 抽象基类
. graphfellow dict的操作 update/get/set
ⅱ. chunks dict[str, list[chunk]]
1. delete_chunks_by_path
2. update_chunks_by_path
3. list_chunks_by_path
4. vector_search/keyword_search
ⅲ. 手写一个bm25检索
ⅳ. 【核心】检索机制 vector bm25 graph 如何进行融合
4. file_watcher @jinli
a. 抽象基类
. on_start:
1. file_store 的start 在前加载graphfile_watcher在后递归扫描目录
a. 通过ms_time对比graphon_change 进行改动
ⅱ. on_change:
1. 更新/增加:
a. delete_chunks_by_path 更新数据库
b. upate_chunks_by_path 更新数据库
c. 更新graph
2. 删除
a. delete_chunks_by_path 更新数据库
传统 RAG 按固定 token 长度切片经常切坏文档结构。ReMe 基于 Markdown AST 做语义分块:
MemorySchema
- 按 H1/H2/H3 章节嵌套建树,递归分块
- **每个 chunk 保留完整标题骨架**——检索到片段后一眼看出它在哪个章节下
- 表格自动重复表头、代码块保留 fence、列表按项打包
1. markdown文件结构 @sen
a. formatter
. name
ⅱ. desc
2. memory文件结构目录
a. MEMORY.md
b. msg/files -> daily/YYYYMMDD/YYYYMMDD.md + xxxx.md
. YYYYMMDD.md
1. xxx -> xxxx.md
2. xxx -> xxxd.md
ⅱ.
c. daily -> topic/topic_l1/topic_l1.md + xxx.md + topic_l2
d. proactive
```
示例 chunk
─────────────────────
# 光伏产业链
## 上游:硅料
### 多晶硅工艺
[chunk 正文] ← 实际内容
## 中游:硅片 ← 骨架(只有标题)
## 下游:组件
─────────────────────
```
steps:
## 特性3自进化
1. 治理(算法+LLM
a. 节点关联P0现有的链接做补充挖掘新的LLM的link
. /Users/yuli/workspace/ReMe/reme2/component/edge_extractor/llm_edge_extractor.py
ⅱ. 移动到steps
b. 节点整合/节点拆分/节点归档
c. 健康度检查
2. retrieve 调用store的检索
3. 原子stepsreme edit
4. 组合steps总结
a. - freq (every_n_turn、compact) -> daily_summarizer
b. topic (/dream ) -> topic_summarizer(daily_xx -> topic_xx)
c. proactive -> proactive_summarizer(personal_xxx -> proactive_query - pre_query
> **ReMe 的记忆不是被动存的,是主动长成知识图谱的。**
```
用户对话 / 外部素材
├───────────────────────────────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ auto-memory│ │auto-resource│
│ 对话→日记 │ │ 素材→解析 │
└─────┬──────┘ └──────┬─────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────┐
│ daily/ │
│ (事件日记 + resource 加工笔记) │
└─────────────────────┬───────────────────────────┘
▼ 定时触发
┌─────────────┐
│ auto-dream │
│ 提炼 + 建图谱 │
└──────┬──────┘
┌─────────────────────────────────────────────────┐
│ digest/ │
│ (知识卡片 + wikilink 互联 = 知识图谱) │
└─────────────────────────────────────────────────┘
```
用户什么都不用做Agent 在后台让笔记自己长出结构。
### 3.1 auto-resource
监控 `resource/` 目录,新文件进来后自动解析内容、整理为结构化笔记写入 `daily/` 下。
### 3.2 auto-memory
对话进行时ReMe 在后台把上下文自动写入当天日记。不是简单的对话摘要——而是一个拥有完整读写能力的 LLM
Agent自己决定记什么、怎么组织、合并还是新增。
### 3.3 auto-dream + auto-link睡眠式记忆整理
借鉴人在睡眠中巩固记忆的机制——把日记和素材提炼成知识卡片,并自动织出图谱关系:
```
┌───────────────────────────┐
│ daily/2026-05-28/xxx.md │ ← 一篇日记或素材
└─────────────┬─────────────┘
╔═════════════════════════════════════╗
║ Phase 1 — Extract一个 Agent
║ "这份材料教了什么道理?" ║
║ ║
║ 输出 N 个抽象单元,各带 bucket 标签 ║
║ (空 → 结束,没东西值得记) ║
╚══════════╤══════════╤═══════════════╝
│ │
┌─────────────┘ └──────────────┐
▼ ▼
╔══════════════════════════════╗ ╔══════════════════════════════╗
║ Phase 2 — Integrate ║ ║ Phase 2 — Integrate ║
║ (每个 unit 独立一个 Agent) ║ ║ (每个 unit 独立一个 Agent) ║
║ ║ ║ ║
║ 1. search + traverse 召回 ║ ║ 1. search + traverse 召回 ║
║ 2. 决策: CREATE / UPDATE ║ ║ 2. 决策: CREATE / UPDATE ║
║ 3. 写入 + 自动织链接 ║ ║ 3. 写入 + 自动织链接 ║
╚══════════════╤═══════════════╝ ╚══════════════╤═══════════════╝
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ digest/ │
│ procedure/key-rotation.md ←─ derived_from:: [[daily/..]] │
│ wiki/credential-compliance.md ─ relates_to:: [[...]] │
│ personal/user-pr-pref.md │
└──────────────────────────────────────────────────────────────┘
知识图谱自动生长
```
**Phase 1 筛选**——多个事实说明同一个道理就合并为一个 unit分到三个桶`procedure`(怎么做)/ `personal`(用户偏好)/ `wiki`
(通用知识)。没东西值得记则流程结束。
**Phase 2 先搜后写**——先搜已有 digest再决策新建CREATE、追加佐证CORROBORATE、补充精度REFINE、修正矛盾CORRECT
**auto-link 是写入的副产品**——写 digest 时自动加 `derived_from:: [[素材]]` 溯源 + `relates_to::` 概念互联,图谱随每次
dream 自动变密。
**CronDreamer 定时批跑**——每天扫描当天所有 daily + resource 文件,逐个执行上述管线。
## 特性4混合索引 + 渐进式展开
```
用户提问: "宁德时代的电池技术?"
├──────────────────────┬──────────────────────────┐
▼ ▼ │
┌─────────────────┐ ┌──────────────────┐ │
│ 全文倒排索引 │ │ 向量索引 │ │
│ (numpy + jieba) │ │ (faiss) │ │
│ │ │ │ │
│ "宁德时代" 精确 │ │ "动力电池龙头" │ │
│ 命中 │ │ 语义近似命中 │ │
└────────┬────────┘ └────────┬─────────┘ │
│ text_weight=0.3 │ vector_weight=0.7 │
└──────────┬──────────┘ │
▼ │
┌───────────────┐ │
│ RRF 融合排序 │ │
│ score = Σ(w/(k+rank)) │
└───────┬───────┘ │
▼ │
┌──────────────────────────────────────┐ │
│ 第一跳Top-K chunk 全文 + 评分 │ │
└───────────────────┬──────────────────┘ │
▼ │
┌──────────────────────────────────────┐ │
│ 第二跳:邻居目录(只有标题,不展开正文)│ ← wikilink 图谱 │
└───────────────────┬──────────────────┘ │
▼ │
┌──────────────────────────────────────┐ │
│ 第 N 跳Agent 按需追问,展开正文 │ │
└──────────────────────────────────────┘ │
```
### 4.1 混合索引构建
两套索引并行维护,各擅其长:
- **全文倒排索引**numpy + jieba BM25——精确匹配专有名词搜"宁德时代"必须命中。纯 Python 实现,增量更新,无原生扩展依赖。
- **向量索引**faiss——语义相似度搜"锂电正极原料"能命中"钴"。基于 embedding 模型生成稠密向量。
### 4.2 基于 RRF 的混合检索
两条通路并行跑(`asyncio.gather`),用 RRFReciprocal Rank Fusion融合排序
```
融合分 = Σ( weight_i / (k + rank_i) ) k=60, vector_weight=0.7, text_weight=0.3
```
为什么要两路?纯向量容易错配名词("苹果公司"≈"水果"),纯关键词抓不到同义改写——融合互补盲区。
### 4.3 渐进式链接展开
传统 RAG 一次性把 Top-K 全塞进上下文token 浪费且噪音多。ReMe 分跳展开,按需深入:
**第一跳** — 返回命中 chunk 全文 + 分数明细
**第二跳** — 展开 wikilink 邻居的"目录"(只有标题,不展开正文):
```
========== digest/wiki/宁德时代.md:5-22 [score=0.0247 vector=0.0156 keyword=0.0091] ==========
# 宁德时代
全球动力电池出货量第一,核心技术为 CTPCell to Pack和钠离子电池……
outlinks (2):
→ digest/wiki/磷酸铁锂.md name="磷酸铁锂正极路线" description="磷酸铁锂与三元路线对比" via predicate=相关技术
→ digest/wiki/固态电池.md name="固态电池技术路线" description="全固态与半固态进展" via predicate=技术演进
inlinks (2):
← daily/2026-03-18/宁德调研.md name="宁德时代调研纪要" description="2026Q1产能与订单跟踪" via plain
← digest/wiki/新能源产业链.md name="新能源产业链全景" description="从锂矿到整车的全链条" via predicate=下游应用
```
**第 N 跳** — Agent 看过"目录"后,自己决定哪些邻居值得深入,再发起 read 拿正文。
二跳目录每条只占一行(最多 10 outlink + 10 inlinkAgent 拥有全局视野却不撑爆上下文。
## 特性5多 Agent 框架集成
ReMe 不做独立 Agent 产品,而是作为**能力层**被任意框架调用:
| 集成路径 | 适用对象 | 方式 |
|---------------------|----------------------|---------------------------------------------|
| SDK 深度集成 | AgentScope / Qwenpaw | middleware 注册 tools + prompthook 注册 auto-* |
| MCP Tool + skill.md | Claude Code | MCP 注册 Tool配 skill.md 开箱即用hook 注册 auto-* |
| HTTP API + CLI | 通用方案 | skill.md + CLI 调用 |
---
# 二、工程架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Service 层HTTP / MCP 双协议) │
│ FastAPI + FastMCP同一套 Job 同时暴露为 REST 和 MCP Tool │
├─────────────────────────────────────────────────────────────────┤
│ Application 层 │
│ 配置加载 → 组件初始化 → Job 注册 → start() / close() 生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ Job 层(编排) │
│ 每个 Job = 一组 Step 的有序管线YAML 声明式配置 │
├─────────────────────────────────────────────────────────────────┤
│ Step 层(业务逻辑) │
│ 原子操作单元按功能域分组file_io / index / evolve / common │
├─────────────────────────────────────────────────────────────────┤
│ Component 层(可插拔基础设施) │
│ 统一注册表 R一行配置切换实现 │
│ file_store / embedding / keyword_index / llm / file_graph │
└─────────────────────────────────────────────────────────────────┘
```
## 2.1 服务层
每个 Job 同时暴露为两种协议,写一次逻辑、两种方式调用:
| 协议 | 传输方式 | 适用场景 |
|---------------|-------------------------------|------------------------------|
| HTTPFastAPI | JSON POST / SSE | REST 调用、Web 前端 |
| MCPFastMCP | stdio / SSE / streamable-http | Claude Code、Cursor 等 MCP 客户端 |
- **按需拉起**Agent 检测到服务未运行时自动后台启动,用户无感知
- **服务发现**:通过 `REME_SERVICE_INFO` 环境变量广播地址,`find_reme` 一键探活
## 2.2 组件系统Component
统一注册表 `R`,所有基础设施都是可插拔的——改一行配置就能切换后端:
| 组件 | 干什么 | 可选后端 |
|-----------------|---------------|-----------------------|
| file_store | 文件存储 + 索引协调 | local |
| file_graph | wikilink 双向图谱 | local / nx / neo4j |
| keyword_index | 全文倒排索引 | bm25numpy + jieba |
| embedding_store | 向量存储与检索 | localfaiss |
| embedding | 文本转向量 | openai 兼容接口 |
| llm | 大模型调用 | anthropic / openai 兼容 |
| tokenizer | 分词 | regex / jieba |
## 2.3 Job 列表
**Job** 是 ReMe 暴露给外部的操作单元——同一个 Job 可以作为 Python 函数直接调用、作为 MCP Tool 被 Agent 使用、也可以作为 CLI
命令执行。
| 类别 | Job | 功能 |
|------|---------------------------|----------------------------------|
| 检索 | `search` | 混合检索(向量 + BM25 + RRF+ 渐进式图展开 |
| 检索 | `traverse` | 从指定路径遍历 wikilink 图谱 |
| 文件读写 | `read` | 读取 markdown 文件内容 |
| 文件读写 | `read_image` | 读取图片文件base64 |
| 文件读写 | `write` | 新建或覆写 markdown 文件(含 frontmatter |
| 文件读写 | `edit` | 文件内查找替换 |
| 文件读写 | `delete` | 删除文件,返回残留入边 |
| 文件读写 | `move` | 移动 / 重命名,自动重写 wikilink |
| 文件读写 | `list` | 列出目录下文件 |
| 文件读写 | `stat` | 文件元信息(大小、修改时间) |
| 文件读写 | `frontmatter_read` | 读取 frontmatter |
| 文件读写 | `frontmatter_update` | 合并更新 frontmatter |
| 文件读写 | `frontmatter_delete` | 删除 frontmatter 字段 |
| 日记管理 | `daily_create` | 幂等创建当天日记文件 |
| 日记管理 | `daily_list` | 列出某天的所有日记 |
| 日记管理 | `daily_reindex` | 重建当天索引页 |
| 索引维护 | `reindex` | 清空并全量重建索引 |
| 索引维护 | `update_store_index_loop` | 后台监听文件变更,增量更新 |
| 自进化 | `auto_memory` | 对话记录写入日记LLM Agent |
| 自进化 | `dream` | 单文件记忆提炼到 digestLLM Agent |
| 自进化 | `auto-dream` | 批量扫描当天文件,逐个 dream |
| 系统 | `health_check` | 组件健康检查 |
| 系统 | `version` | 返回版本号 |
| 系统 | `help` | 列出所有已注册 Job |

231
docs4/reme_scene.md Normal file
View file

@ -0,0 +1,231 @@
# ReMe 应用场景
## 金融场景:产业链知识库
**主角**:王分析师,新能源行业研究员,每天处理 10+ 篇研报、数十条产业新闻、若干场公司调研。
**痛点**信息散落在飞书文档、PDF 研报、微信群消息、调研纪要里,"上次调研宁德时代时聊到的钴价话题"再也找不回来。
### 一周内 ReMe 自动织出的产业链图谱
---
#### Day 1周一盘后素材摄入 + 对话
王分析师把今天看到的 3 篇研报扔进 `resource/`,又和 Agent 口述了对刚果(金)矿权变更的看法:
```
对话片段:
> 今天嘉能可发了三季报,钴产量同比下滑 18%……
> 刚果(金)那边的政策变化,对洛阳钼业 KFM 矿的影响要重点跟……
> 下游三元正极厂商已经开始转向高镍低钴方案……
```
**auto-memory** 实时把对话写入当天日记;**auto-resource** 自动解析研报写入加工笔记:
```
daily/
├── 2026-05-18.md ← 当天索引页,汇总所有事件
└── 2026-05-18/
├── session_001.md ← auto-memory 写入的对话日志
│ (含嘉能可三季报、刚果金矿权、高镍化趋势等事件)
├── resource_001.md ← auto-resource 对研报 1 的加工笔记
├── resource_002.md ← 研报 2 加工笔记
└── resource_003.md ← 研报 3 加工笔记
```
**Day 1 夜间 auto-dream**——CronDreamer 扫描当天 4 个文件,逐个执行 Extract → Integrate 管线:
处理 `session_001.md`
- **Phase 1 Extract**:从对话日志中提取 3 个抽象单元——「嘉能可钴产量下滑」(wiki)、「刚果金矿权政策风险」(wiki)、「三元正极高镍化趋势」(wiki)
- **Phase 2 Integrate**(每个 unit 独立一个 Agent
- 搜索已有 digest均无匹配 → 决策 **CREATE**
- 新建 `digest/wiki/嘉能可.md``digest/wiki/钴.md``digest/wiki/三元正极.md`
- 写入时自动织链接:`derived_from:: [[daily/2026-05-18/session_001]]`,以及概念互联 `relates_to:: [[三元正极]]`
处理 `resource_001.md`(嘉能可三季报):
- **Phase 1**:提取「嘉能可钴业务财务数据」(wiki)
- **Phase 2**:搜索到刚刚新建的 `digest/wiki/嘉能可.md` → 决策 **CORROBORATE**,追加财务佐证段落
Day 1 结束时 `digest/wiki/` 下新增:
```
digest/wiki/
├── 嘉能可.md ← CREATE + CORROBORATE研报佐证
├── 钴.md ← CREATE
└── 三元正极.md ← CREATE
```
`钴.md` 长这样:
```markdown
---
name: 钴
description: 锂电正极材料关键原料,主产区刚果(金)
tags: [新能源, 原料, 钴]
---
所属领域:: [[新能源]]
下游产品:: [[三元正极]]
# 钴
## 供给端
主要生产商 [[嘉能可]],产能集中于刚果(金)。
嘉能可三季度钴产量同比下滑 18%。
derived_from:: [[daily/2026-05-18/session_001]]
## 政策风险
刚果(金)矿权政策变化,可能影响 KFM 矿运营。
derived_from:: [[daily/2026-05-18/session_001]]
```
---
#### Day 2周二宁德时代调研
王分析师参加宁德时代调研会后,和 Agent 聊调研要点:
```
> 宁德今年全面切换 9 系高镍三元,钴用量还会继续降……
> 产能利用率 85%,比上季度高 5 个点……
```
auto-memory 写入 `daily/2026-05-19/session_001.md`
**Day 2 夜间 auto-dream** 处理这份 session
- **Phase 1**:提取「宁德时代高镍切换」(wiki)、「宁德时代产能利用率」(wiki)
- **Phase 2**
- 「宁德时代高镍切换」→ 搜索到 `digest/wiki/三元正极.md` 已存在高镍化内容 → 决策 **REFINE**,补充"宁德 9 系切换"作为具体案例,并新建 `digest/wiki/宁德时代.md`
- 「产能利用率」→ 无匹配 → 写入 `digest/wiki/宁德时代.md`(已存在,追加章节)
Day 2 结束时图谱新增节点和边:
```
digest/wiki/
├── 嘉能可.md
├── 钴.md
├── 三元正极.md ← REFINE新增宁德 9 系切换案例
└── 宁德时代.md ← CREATE含高镍切换 + 产能数据
relates_to:: [[三元正极]]
relates_to:: [[钴]]
```
---
#### Day 3周三亿纬电话会 + 洛阳钼业跟踪
两场对话产生两份 session。夜间 auto-dream 逐个处理:
- `session_001.md`(亿纬电话会)→ Phase 2 搜到 `宁德时代.md``三元正极.md` → CREATE `digest/wiki/亿纬锂能.md`,并在 `三元正极.md` 上 CORROBORATE 高镍趋势
- `session_002.md`(洛阳钼业跟踪)→ Phase 2 搜到 `钴.md` → REFINE `钴.md`,补充洛阳钼业 KFM 矿最新动态CREATE `digest/wiki/洛阳钼业.md`
Day 3 结束时图谱:
```
digest/wiki/
├── 嘉能可.md
├── 洛阳钼业.md ← CREATE
├── 钴.md ← REFINE补充洛阳钼业信息
│ relates_to:: [[嘉能可]], [[洛阳钼业]], [[三元正极]]
├── 三元正极.md ← CORROBORATE亿纬佐证
│ relates_to:: [[钴]], [[宁德时代]], [[亿纬锂能]]
├── 宁德时代.md
└── 亿纬锂能.md ← CREATE
```
每个节点都是**当天 dream 从一份 daily 文件中提取并整合的结果**,不存在跨天"聚合"——跨文件的关联通过 Phase 2 的 search 自然发现已有 digest从而把新信息写入正确的位置。
---
#### Day 5周五用户主动检索
王分析师准备组会要讲新能源板块,主动问 Agent
> **"帮我分析一下锂电相关上下游"**
Agent 调用 ReMe 的 `search` Job走向量 + BM25 + RRF 融合检索,命中已有的 digest 节点,并通过渐进式展开获取上下游全貌:
**第一跳——直接命中 chunk 全文 + 评分**
```
digest/wiki/钴.md:5-22 [score=0.0234 vector=0.0156 keyword=0.0078]
# 钴 / ## 供给端
主要生产商嘉能可、洛阳钼业,产能集中于刚果(金)……
digest/wiki/三元正极.md:10-30 [score=0.0211 vector=0.0148 keyword=0.0063]
# 三元正极 / ## 高镍低钴路线
2026 年起主流厂商加速 9 系产品,宁德已全面切换……
digest/wiki/宁德时代.md:1-20 [score=0.0193 vector=0.0135 keyword=0.0058]
digest/wiki/嘉能可.md:5-30 [score=0.0167 vector=0.0117 keyword=0.0050]
digest/wiki/洛阳钼业.md:1-22 [score=0.0152 vector=0.0106 keyword=0.0046]
```
**第二跳——展开 wikilink 邻居目录(只有标题,不展开正文)**
```
digest/wiki/钴.md 的邻居:
outlinks (3):
→ digest/wiki/三元正极.md name="三元正极" description="高镍低钴技术路线" via predicate=下游产品
→ digest/wiki/嘉能可.md name="嘉能可" description="全球钴业巨头" via predicate=relates_to
→ digest/wiki/洛阳钼业.md name="洛阳钼业" description="KFM 矿运营商" via predicate=relates_to
inlinks (2):
← daily/2026-05-18/session_001.md name="盘后对话" via plain
← daily/2026-05-19/session_001.md name="宁德调研" via plain
digest/wiki/三元正极.md 的邻居:
outlinks (2):
→ digest/wiki/宁德时代.md name="宁德时代" description="全球动力电池龙头" via predicate=relates_to
→ digest/wiki/钴.md name="钴" description="锂电正极关键原料" via predicate=relates_to
inlinks (1):
← digest/wiki/亿纬锂能.md name="亿纬锂能" description="动力电池厂商" via predicate=relates_to
```
**第 N 跳——Agent 按需深入**Agent 看过目录后,决定展开 `亿纬锂能.md` 的正文获取补充信息,调用 `read` Job 拉取。
Agent 基于检索结果**直接回复**王分析师:
> "锂电产业链分三段:上游钴矿(嘉能可、洛阳钼业,刚果金集中)、中游三元正极(高镍化加速)、下游电池厂(宁德/亿纬)。这周你提到的事件分别落在:嘉能可三季报 → 上游产能收缩;高镍化趋势 → 中游路线切换;宁德 9 系切换 → 下游需求验证。"
---
#### Day 7图谱已经长出层次
经过一周每天的 auto-dream 逐文件处理,图谱自然生长出来:
```
┌─────────────┐
┌────────►│ 锂电 │◄────────┐
│ └──────┬──────┘ │
│ upstream │ │ upstream
│ │ 下游产品 │
┌───────┴──────┐ ▼ ┌──────┴──────┐
│ 钴 │ ┌─────────┐ │ 锂 │
│ (刚果金产区) │◄──┤ 原料 ├────►│ (盐湖产区) │
└───────┬──────┘ └────┬────┘ └─────────────┘
│ relates_to │ relates_to
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 嘉能可 │ │ 三元正极 │◄── 高镍化趋势
│ 洛阳钼业 │ └──────┬───────┘
└──────────────┘ │ relates_to
┌──────────────┐
│ 宁德时代 │ ← Day 2 调研
│ 亿纬锂能 │ ← Day 3 电话会
└──────────────┘
```
**没有一个节点是凭空编造的**——每条边对应笔记里的一句 `relates_to:: [[X]]``derived_from:: [[daily/...]]`,每个节点点开就是 Markdown每段内容都能追溯到原始 daily 事件。图谱不是一次性生成的,而是每天 dream 一点、链接一点,渐进生长出来的。
---
### ReMe 在这个场景下的核心价值
分析师只负责"看 + 说",知识图谱自己长出来:
- **auto-memory** 把每次对话实时写入当天日记
- **auto-dream** 每天逐文件执行 Extract → Integrate 管线,先搜已有 digest 再决策CREATE / CORROBORATE / REFINE / CORRECT知识卡片渐进生长
- **auto-link** 是 dream 写入的副产品——`derived_from::` 溯源 + `relates_to::` 概念互联,图谱随每次 dream 自动变密
- **混合检索 + 渐进展开** 让 Agent 先看骨架再决定深入哪个节点,不把 Top-K 全文塞进上下文

View file

@ -48,7 +48,7 @@ class Application(BaseComponent):
cfg = self.config
vault_path = Path(cfg.vault_dir).absolute()
vault_path.mkdir(parents=True, exist_ok=True)
for subdir in [cfg.metadata_dir, cfg.daily_dir, cfg.digest_dir, cfg.resource_dir]:
for subdir in [cfg.metadata_dir, cfg.resource_dir, cfg.dialog_dir, cfg.daily_dir, cfg.digest_dir]:
if subdir:
(vault_path / subdir).mkdir(parents=True, exist_ok=True)

View file

@ -475,7 +475,7 @@ components:
supported_extensions: [ "md" ]
chunked:
backend: chunked
supported_extensions: [ "txt", "html", "json", "yaml", "py" ]
supported_extensions: [ "txt", "html", "json", "jsonl", "yaml", "py" ]
default:
backend: default

View file

@ -28,28 +28,20 @@ class ApplicationConfig(BaseModel):
"""Root config for the ReMe application."""
app_name: str = Field(default=os.getenv("APP_NAME", "ReMe"), description="Application display name")
vault_dir: str = Field(default=".reme", description="Vault root directory (knowledge base) for runtime files")
vault_dir: str = Field(default=".reme", description="Vault root directory for runtime files")
metadata_dir: str = Field(default="reme_metadata", description="Subdirectory for ReMe persistent state")
resource_dir: str = Field(default="resource", description="Subdirectory for external assets")
dialog_dir: str = Field(default="dialog", description="Subdirectory for dialog memory")
daily_dir: str = Field(default="daily", description="Subdirectory for daily memory")
digest_dir: str = Field(default="digest", description="Subdirectory for digest")
resource_dir: str = Field(
default="resource",
description="Subdirectory for passively-received external assets (upload bucket)",
)
digest_dir: str = Field(default="digest", description="Subdirectory for digest memory")
enable_logo: bool = Field(default=True, description="Show ASCII logo on startup")
language: str = Field(default="", description="Default language for LLM interactions")
log_to_console: bool = Field(default=True, description="Log to console")
log_to_file: bool = Field(default=True, description="Log to file")
mcp_servers: dict[str, dict] = Field(default_factory=dict, description="MCP server configs by name")
service: ComponentConfig = Field(default_factory=ComponentConfig, description="Service endpoint config")
jobs: dict[str, JobConfig] = Field(
default_factory=dict,
description="Job definitions keyed by job name",
)
thread_pool_max_workers: int = Field(
default=0,
description="Max worker threads in the shared thread pool; 0 to disable",
)
jobs: dict[str, JobConfig] = Field(default_factory=dict, description="Job definitions keyed by job name")
thread_pool_max_workers: int = Field(default=0, description="Max worker threads; 0 to disable")
components: dict[ComponentEnum, dict[str, ComponentConfig]] = Field(
default_factory=dict,
description="Component registry keyed by type then name",

View file

@ -43,51 +43,6 @@ async def _make_app() -> Application:
return app
def test_llm_demo_step_basic_chat():
"""LLMDemoStep drives Agent through self.llm."""
async def run():
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
app = await _make_app()
try:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="What is 1 + 1? Reply with just the number.",
)
text = (response.answer or "").strip()
print(f"\n[basic_chat] response: {text!r}")
assert text, "Empty assistant response"
assert "2" in text, f"Expected '2' in response, got: {text!r}"
print("✓ test_llm_demo_step_basic_chat passed")
finally:
await app.close()
asyncio.run(run())
def test_llm_demo_step_with_tool():
"""LLMDemoStep registers the add tool and the agent invokes it."""
async def run():
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
app = await _make_app()
try:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="Use the add tool to compute 21 + 21 and report the result.",
sys_prompt="Use the `add` tool whenever the user asks to add numbers.",
use_add_tool=True,
)
text = (response.answer or "").strip()
print(f"\n[with_tool] response: {text!r}")
assert "42" in text, f"Expected '42' in response, got: {text!r}"
print("✓ test_llm_demo_step_with_tool passed")
finally:
await app.close()
asyncio.run(run())
class MathResult(BaseModel):
"""Structured output for a math computation."""
@ -110,66 +65,78 @@ class SentimentAnalysis(BaseModel):
)
def test_llm_demo_step_structured_output():
"""LLMDemoStep generates structured output via generate_structured_output."""
async def run():
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
app = await _make_app()
try:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="What is 15 multiplied by 7? Show your work.",
sys_prompt="You are a math tutor. Solve the problem step by step.",
structured_model=MathResult,
)
structured = response.metadata.get("structured_output")
print(f"\n[structured_output] result: {structured}")
assert structured is not None, "structured_output should not be None"
assert "result" in structured, "structured_output should have 'result' field"
assert structured["result"] == 105, f"Expected result=105, got: {structured['result']}"
assert "expression" in structured, "structured_output should have 'expression' field"
assert "explanation" in structured, "structured_output should have 'explanation' field"
print("✓ test_llm_demo_step_structured_output passed")
finally:
await app.close()
asyncio.run(run())
async def _run_basic_chat(app: Application) -> None:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="What is 1 + 1? Reply with just the number.",
)
text = (response.answer or "").strip()
print(f"\n[basic_chat] response: {text!r}")
assert text, "Empty assistant response"
assert "2" in text, f"Expected '2' in response, got: {text!r}"
print("✓ test_llm_demo_step_basic_chat passed")
def test_llm_demo_step_structured_output_enum():
"""LLMDemoStep structured output with Literal/enum fields."""
async def _run_with_tool(app: Application) -> None:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="Use the add tool to compute 21 + 21 and report the result.",
sys_prompt="Use the `add` tool whenever the user asks to add numbers.",
use_add_tool=True,
)
text = (response.answer or "").strip()
print(f"\n[with_tool] response: {text!r}")
assert "42" in text, f"Expected '42' in response, got: {text!r}"
print("✓ test_llm_demo_step_with_tool passed")
async def run():
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
app = await _make_app()
try:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="Analyze the sentiment: 'I absolutely love this product! It exceeded all my expectations.'",
sys_prompt="You are a sentiment analysis expert. Analyze the given text.",
structured_model=SentimentAnalysis,
)
structured = response.metadata.get("structured_output")
print(f"\n[structured_enum] result: {structured}")
assert structured is not None, "structured_output should not be None"
assert (
structured["sentiment"] == "positive"
), f"Expected sentiment='positive', got: {structured['sentiment']}"
assert 0 <= structured["confidence"] <= 1, f"Confidence should be 0-1, got: {structured['confidence']}"
assert isinstance(structured["key_phrases"], list), "key_phrases should be a list"
assert len(structured["key_phrases"]) > 0, "key_phrases should not be empty"
print("✓ test_llm_demo_step_structured_output_enum passed")
finally:
await app.close()
asyncio.run(run())
async def _run_structured_output(app: Application) -> None:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="What is 15 multiplied by 7? Show your work.",
sys_prompt="You are a math tutor. Solve the problem step by step.",
structured_model=MathResult,
)
structured = response.metadata.get("structured_output")
print(f"\n[structured_output] result: {structured}")
assert structured is not None, "structured_output should not be None"
assert "result" in structured, "structured_output should have 'result' field"
assert structured["result"] == 105, f"Expected result=105, got: {structured['result']}"
assert "expression" in structured, "structured_output should have 'expression' field"
assert "explanation" in structured, "structured_output should have 'explanation' field"
print("✓ test_llm_demo_step_structured_output passed")
async def _run_structured_output_enum(app: Application) -> None:
step = LLMDemoStep(app_context=app.context)
response = await step(
query="Analyze the sentiment: 'I absolutely love this product! It exceeded all my expectations.'",
sys_prompt="You are a sentiment analysis expert. Analyze the given text.",
structured_model=SentimentAnalysis,
)
structured = response.metadata.get("structured_output")
print(f"\n[structured_enum] result: {structured}")
assert structured is not None, "structured_output should not be None"
assert structured["sentiment"] == "positive", f"Expected sentiment='positive', got: {structured['sentiment']}"
assert 0 <= structured["confidence"] <= 1, f"Confidence should be 0-1, got: {structured['confidence']}"
assert isinstance(structured["key_phrases"], list), "key_phrases should be a list"
assert len(structured["key_phrases"]) > 0, "key_phrases should not be empty"
print("✓ test_llm_demo_step_structured_output_enum passed")
async def _run_all() -> None:
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
app = await _make_app()
try:
await _run_basic_chat(app)
await _run_with_tool(app)
await _run_structured_output(app)
await _run_structured_output_enum(app)
finally:
await app.close()
if __name__ == "__main__":
print("=== LLMDemoStep + Agent integration tests ===")
test_llm_demo_step_basic_chat()
test_llm_demo_step_with_tool()
test_llm_demo_step_structured_output()
test_llm_demo_step_structured_output_enum()
asyncio.run(_run_all())
print("\nAll integration tests passed!")