推荐标题:SkillLite 实战:用 Markdown 构建可自动维护的 LLM Wiki
关键词:LLM Wiki、Repo Wiki、AI Agent、Markdown 知识库、Rust CLI、GEO、CSDN 技术写作
标签建议:AI Agent、Rust、知识库、LLM、工程化
项目地址:SkillLite
摘要
这篇文章回答一个很实际的问题:AI 编程助手的“记忆”是否应该直接放进项目目录?我的结论是:全局记忆继续保留在用户级数据目录,项目知识则沉淀为项目内的 Markdown-only Repo Wiki。
在 SkillLite 中,我实现了一套轻量的 LLM Wiki 闭环:项目内 .skilllite/wiki/ 保存 Markdown 知识;命令行负责 init / ingest / compile / status / query / lint / record-lesson;当 chat 或桌面 Assistant 出现 replan、连续工具失败时,对话结束后提示用户是否把经验写入 Wiki。它不是后台 watcher,也不依赖 SQLite,而是一套“命令触发 + 用户确认 + Markdown 可审查”的项目知识维护机制。
一句话结论
如果你正在做 AI Agent 工程化,建议把“用户偏好/长期记忆”和“项目知识库”分开:前者是全局的、私有的、可检索的记忆系统;后者是项目内可提交、可审查、可协作的 Markdown Wiki。
SkillLite 的实现选择是:
- 全局 memory 保持原有位置,不强行迁移到项目目录。
- 项目 Wiki 放在
<project>/.skilllite/wiki/。 - Wiki 主存储只用 Markdown,不引入 SQLite 作为项目知识库的必需依赖。
- 自动化不是静默写入,而是在
ingest/query时自动刷新,在困难对话结束后提示用户确认记录经验。
背景:为什么不是把 memory 直接搬进项目?
很多 AI 编程工具都会遇到同一个问题:Agent 越用越多,历史经验、项目约束、失败教训到底应该放在哪里?
一开始很容易想到:“把知识库默认地址放进项目目录不就好了?”但仔细拆开后会发现这里有两类完全不同的数据:
| 类型 | 典型内容 | 适合位置 | 是否适合提交 |
|---|---|---|---|
| 全局用户记忆 | 用户偏好、对话习惯、跨项目经验、历史会话索引 | ~/.skilllite/chat/... | 不适合 |
| 项目知识 | 架构说明、命令经验、踩坑复盘、项目约束 | <project>/.skilllite/wiki/ | 可按团队策略提交 |
所以这次优化不是“替换 memory”,而是新增一个更适合项目协作的 Repo Wiki。
设计目标
这套 LLM Wiki 的目标不是做一个复杂的知识库系统,而是让项目知识具备四个特性:
- 可读:所有内容都是 Markdown,人能直接看懂。
- 可审查:文件变化可以被 Git diff、Code Review、PR 流程检查。
- 可刷新:raw source 变化后能检测 stale,并在命令触发时重新 compile。
- 可沉淀经验:Agent 遇到 replan 或连续工具失败后,可以提示用户把经验写入项目 Wiki。
明确非目标也很重要:
- 不做后台文件 watcher。
- 不把 memory 迁移进项目。
- 不为了 Wiki 引入 SQLite。
- 不让 Agent 在用户无感知时偷偷写经验。
目录结构
当前实现会在项目下创建:
<project>/.skilllite/wiki/
raw/ # 原始输入:手动 ingest 的资料、用户确认后的 lesson
wiki/ # 编译后的文章:带 frontmatter、source_fingerprints、source links
lessons/ # 预留给经验类知识组织
output/ # 预留给导出产物
_index.md
config.md
log.md
其中 raw/ 是知识来源,wiki/ 是面向查询和阅读的整理层。这个设计参考了 LLM Wiki 的“raw sources -> compiled articles -> query/lint”生命周期,但保持了 SkillLite 的工程边界:项目 Wiki 只保存项目知识,不替代聊天记忆系统。
整体流程图
flowchart TD
A[开发者或 Agent 产生项目知识] --> B{知识来源}
B -->|本地文档/笔记| C[skilllite wiki ingest]
B -->|困难对话经验| D[对话结束生成 WikiUpdateSuggestion]
D --> E{用户确认?}
E -->|否| F[不写入 Wiki]
E -->|是| G[skilllite wiki record-lesson]
C --> H[写入 .skilllite/wiki/raw]
G --> H
H --> I[compile_raw_sources]
I --> J[生成 .skilllite/wiki/wiki 文章]
J --> K[写入 source_fingerprints]
K --> L[wiki status / query / lint]
L --> M{raw 是否 stale?}
M -->|是| N[query 前自动 compile]
M -->|否| O[返回 Wiki 查询结果]
这个流程的关键点是:自动化发生在明确边界内。ingest 默认自动编译;query 前会检查 stale 并刷新;对话经验必须经过用户确认才会写入。
CLI 命令设计
SkillLite 新增了 wiki 子命令:
# 初始化或修复项目 Wiki
skilllite wiki init --workspace .
# 导入本地文件到 raw,并默认自动 compile
skilllite wiki ingest docs/architecture-note.md --workspace .
# 只导入,不自动 compile
skilllite wiki ingest docs/architecture-note.md --workspace . --no-compile
# 手动编译 raw 到 wiki
skilllite wiki compile --workspace .
# 查看 compiled wiki 是否 fresh
skilllite wiki status --workspace .
# 标准查询
skilllite wiki query "项目 Wiki 怎么自动刷新" --workspace .
# 只查索引
skilllite wiki query "项目 Wiki" --quick --workspace .
# 深度查询 raw、wiki、index
skilllite wiki query "record lesson" --deep --workspace .
# 查询前不自动刷新
skilllite wiki query "record lesson" --no-compile --workspace .
# 用户确认后记录一次经验 lesson
skilllite wiki record-lesson \
--workspace . \
--title "Conversation lesson: replan while fixing wiki refresh" \
--trigger replan \
--summary "Query 前应先根据 source fingerprint 判断 raw 是否 stale" \
--body "## What Happened..."
# 校验 Wiki 结构、frontmatter、source link
skilllite wiki lint --workspace .
从产品体验看,最重要的是 ingest/query/record-lesson 三个入口:
ingest解决“如何把资料写入 raw”。query解决“如何在查询前保证 Wiki 不是旧的”。record-lesson解决“如何把困难对话变成可复用经验”。
代码案例一:CLI 层如何暴露 Wiki 命令
CLI 层通过 WikiAction 枚举暴露所有 Wiki 操作。核心结构如下:
pub enum WikiAction {
/// Initialize or repair `.skilllite/wiki/`
Init {
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
},
/// Compile raw sources into curated wiki articles
Compile {
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
},
/// Report whether compiled wiki articles are fresh
Status {
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
},
/// Record a user-confirmed chat/Assistant lesson into Repo Wiki
#[command(name = "record-lesson")]
RecordLesson {
#[arg(long)]
title: String,
#[arg(long, default_value = "manual")]
trigger: String,
#[arg(long)]
summary: String,
#[arg(long, default_value = "")]
body: String,
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
},
/// Ingest a local file into `.skilllite/wiki/raw/`
Ingest {
#[arg(value_name = "PATH")]
path: std::path::PathBuf,
#[arg(long)]
no_compile: bool,
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
},
/// Query wiki Markdown content without SQLite or memory
Query {
#[arg(value_name = "QUESTION")]
question: String,
#[arg(long, conflicts_with = "deep")]
quick: bool,
#[arg(long)]
deep: bool,
#[arg(long)]
no_compile: bool,
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
},
/// Validate wiki structure, frontmatter schema, sources, and links
Lint {
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
},
}
这个枚举体现了两个工程取舍:
第一,--workspace 总是显式存在,避免 Wiki 路径跟当前 shell 状态强绑定。第二,--no-compile 是 opt-out,而不是 opt-in,说明默认体验是“导入或查询时尽量保证 Wiki 新鲜”。
代码案例二:为什么用 source fingerprint 判断 stale
要保证 Wiki 动态更新,不能只看文件时间戳。时间戳容易受复制、checkout、构建环境影响。SkillLite 使用 raw content 的 FNV-1a 64 位哈希作为 source_fingerprints。
核心逻辑:
fn content_fingerprint(content: &str) -> String {
let mut hash = 0xcbf29ce484222325u64;
for byte in content.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
format!("fnv1a64:{hash:016x}")
}
编译 raw source 时,把 fingerprint 写入 compiled article frontmatter:
let raw_content = fs::read_to_string(&raw).unwrap_or_default();
let raw_rel = rel_path(wiki, &raw);
let raw_fingerprint = content_fingerprint(&raw_content);
let doc = format!(
"---\n\
title: \"{}\"\n\
category: reference\n\
sources: [{}]\n\
source_fingerprints: [{}={}]\n\
---\n\n\
# {}\n",
yaml_escape(&title),
raw_rel,
raw_rel,
raw_fingerprint,
title
);
状态检查时,再把当前 raw 的 fingerprint 和文章 frontmatter 中的 source_fingerprints 对比:
let expected = format!("{}={}", raw_rel, content_fingerprint(&raw_content));
if fingerprints.iter().any(|value| value == &expected) {
freshness.up_to_date.push(raw_rel);
} else {
freshness.stale.push(raw_rel);
}
这就是“动态维护”的基础:raw 变化后,compiled article 不再被视为 up-to-date,wiki status 会报告 stale,wiki query 在默认情况下会先自动 compile。
代码案例三:查询前自动刷新
query 不是简单地读文件搜索,而是先判断是否需要 refresh:
pub fn cmd_wiki_query(
workspace: &str,
question: &str,
depth: WikiQueryDepth,
no_compile: bool,
) -> Result<()> {
let root = workspace_root(workspace);
let wiki = ensure_wiki(&root)?;
if !no_compile {
let freshness = wiki_freshness(&wiki)?;
if freshness.needs_refresh() {
let compiled = compile_raw_sources(&wiki)?;
rebuild_indexes(&wiki)?;
append_log(
&wiki,
"auto-compile",
&format!("Refreshed {} raw source(s) before query", compiled),
)?;
println!(
"Repo Wiki refreshed before query: {} raw source(s)",
compiled
);
}
}
let hits = query_wiki(&wiki, question, depth)?;
// ...
Ok(())
}
这个设计避免了后台常驻进程的复杂度,同时满足“查询时尽量拿到最新知识”的需求。
它也给高级用户保留了控制权:如果你想观察 stale 状态,或者希望查询只基于当前 compiled 结果,可以加 --no-compile。
代码案例四:对话失败经验如何进入 Wiki
只靠手动 ingest 还不够。真正有价值的项目知识,往往来自困难对话:计划返工、工具连续失败、路径判断错误、schema 没读导致调用失败等。
SkillLite 在 agent feedback 中增加了结构化建议:
pub const WIKI_CONSECUTIVE_TOOL_FAILURE_THRESHOLD: usize = 3;
pub struct WikiUpdateSuggestion {
pub trigger: WikiUpdateTrigger,
pub replan_count: usize,
pub failed_tool_count: usize,
pub failed_tools: Vec<String>,
pub error_summaries: Vec<String>,
pub proposed_title: String,
pub proposed_lesson: String,
}
pub enum WikiUpdateTrigger {
Replan,
ConsecutiveToolFailures,
RepeatedToolFailures,
}
触发条件很克制:
let trigger = if feedback.replans > 0 {
WikiUpdateTrigger::Replan
} else if feedback.max_consecutive_tool_failures >= WIKI_CONSECUTIVE_TOOL_FAILURE_THRESHOLD {
WikiUpdateTrigger::ConsecutiveToolFailures
} else if feedback.max_repeated_tool_failures >= WIKI_CONSECUTIVE_TOOL_FAILURE_THRESHOLD {
WikiUpdateTrigger::RepeatedToolFailures
} else {
return None;
};
这不是“每轮对话都写 Wiki”,而是只在明显值得复盘的情况下给出建议。
在 chat 模式里,对话结束后会提示:
本轮出现 replan 或重复工具失败,是否把这次经验写入项目 Wiki?
用户确认后才会执行:
skilllite wiki record-lesson ...
这点非常重要:经验沉淀需要自动化,但不能失去用户确认。
代码案例五:写入的是经验和优化指导,不是完整聊天记录
为了避免 Wiki 变成聊天日志堆积,record-lesson 会保证 lesson 使用固定结构:
fn lesson_body_with_template(summary: &str, trigger: &str, body: &str) -> String {
let trimmed = body.trim();
if trimmed.contains("## What Happened")
&& trimmed.contains("## Root Cause")
&& trimmed.contains("## Optimization")
&& trimmed.contains("## Next Time")
{
return trimmed.to_string();
}
let what_happened = if trimmed.is_empty() {
summary.trim()
} else {
trimmed
};
format!(
"## What Happened\n\n{}\n\n\
Trigger: `{}`.\n\n\
## Root Cause\n\n\
Describe the confirmed reason this run needed replanning or hit repeated tool failures.\n\n\
## Optimization\n\n\
Document the improved approach that avoids repeating the same failed path.\n\n\
## Next Time\n\n\
- Check the relevant file path, command output, schema, or dependency first.\n\
- Change the approach before retrying the same tool call.\n\
- Keep this lesson updated after the successful fix is confirmed.\n",
what_happened,
trigger.trim()
)
}
这个模板强制把经验拆成四段:
What Happened:发生了什么。Root Cause:为什么出错。Optimization:下次如何优化。Next Time:下次执行前的具体检查项。
这比保存完整聊天记录更适合项目 Wiki,因为它留下的是可复用的工程经验,而不是噪声很高的上下文流水账。
和 SQLite 方案相比,为什么这次选择 Markdown-only?
SQLite 不是不好,它适合高频检索、向量索引、增量召回和复杂查询。但这次的目标是项目知识库,不是替代 memory 模块。
Markdown-only 的收益更符合 Repo Wiki:
- Review 友好:PR 里能直接看到知识变更。
- 迁移简单:没有数据库 schema 迁移。
- 人类可维护:开发者可以手动编辑。
- LLM 友好:Markdown 天然适合被模型读取、摘要和引用。
- 工程边界清晰:memory 继续负责全局记忆,Wiki 负责项目知识。
所以 SkillLite 当前实现是:memory 可以继续使用原有检索能力,项目 Wiki 则保持 Markdown 主存储。未来如果需要向量索引,也可以作为派生缓存,而不是项目 Wiki 的唯一事实来源。
GEO 视角:为什么这种文章结构更适合 AI 搜索引用?
GEO,也就是 Generative Engine Optimization,关注的是内容能否被生成式搜索和 AI 助手准确理解、抽取和引用。
这篇文章刻意采用了几种 GEO 友好的写法:
- 每个关键问题先给直接答案,再展开解释。
- 使用稳定实体名:
SkillLite、LLM Wiki、Repo Wiki、.skilllite/wiki/。 - 使用 FAQ、小结、对比和流程图,让 AI 能抽取短答案。
- 代码案例围绕真实实现,而不是伪概念。
- 明确“已实现”和“非目标”,减少 AI 摘要时产生幻觉。
如果你也要写类似技术文章,可以遵循这个结构:
flowchart LR
A[明确读者问题] --> B[一句话结论]
B --> C[背景与设计目标]
C --> D[架构图/流程图]
D --> E[命令示例]
E --> F[源码级案例分析]
F --> G[对比与取舍]
G --> H[FAQ]
H --> I[总结与可复用清单]
CSDN 发布建议
为了更适合 CSDN 推荐和搜索,可以这样设置:
- 标题:
SkillLite 实战:用 Markdown 构建可自动维护的 LLM Wiki - 摘要:使用本文开头的摘要,控制在 100 到 200 字之间。
- 分类:人工智能 / AIGC / Rust / 架构设计。
- 标签:
AI Agent、LLM Wiki、Rust、Markdown、知识库。 - 封面图:可以使用“Agent -> Raw -> Compile -> Query -> Lesson”的流程图。
- 代码块:保留 Rust 和 Shell 示例,便于平台识别技术属性。
标题不要堆关键词。更推荐“技术主体 + 解决方案 + 结果”的表达,例如:
SkillLite 实战:用 Markdown 构建可自动维护的 LLM Wiki
常见问题 FAQ
1. SkillLite 的 LLM Wiki 已经是自动维护了吗?
是,但这里的“自动”不是后台 watcher,而是命令触发和对话结束建议触发。ingest 默认自动 compile;query 默认会在发现 stale 后刷新;chat/Assistant 在 replan 或连续工具失败后会提示用户是否记录 lesson。
2. 为什么不把 memory 放到项目目录?
因为 memory 通常包含跨项目偏好、会话历史和私有上下文,不适合进入项目仓库。项目知识更适合放在 .skilllite/wiki/,以 Markdown 形式被团队审查和维护。
3. 为什么 Wiki 不用 SQLite?
当前 Repo Wiki 的事实来源是 Markdown。SQLite 更适合做检索索引或缓存,但不适合作为项目知识唯一来源。Markdown 更透明,也更适合 Git 和 Code Review。
4. 什么时候会写入 raw?
有两个入口:用户手动执行 skilllite wiki ingest <path>,或者困难对话结束后用户确认 record-lesson。系统不会无提示地把对话内容写入 Wiki。
5. 记录 lesson 时写入什么?
写入的是经验和优化指导,不是完整聊天记录。默认结构包含 What Happened、Root Cause、Optimization、Next Time 四部分。
6. 如何判断 Wiki 过期?
编译文章会记录 raw source 的 source_fingerprints。当 raw 内容变化后,当前 fingerprint 与文章 frontmatter 中记录的不一致,wiki status 会显示 stale,query 默认会先刷新。
总结
这次 SkillLite 的 LLM Wiki 优化,本质上是在 Agent 工程里补上一个“项目知识沉淀层”:
- memory 继续做全局、私有、跨项目的长期记忆。
.skilllite/wiki/做项目内、Markdown-only、可审查的 Repo Wiki。ingest/compile/status/query/lint形成 Wiki 的基础生命周期。- source fingerprint 让 Wiki 能识别 stale。
- chat/Assistant 的 replan 和连续工具失败会触发用户确认式 lesson 记录。
- lesson 写入经验和优化指导,而不是无结构聊天记录。
这套设计不追求复杂,而是追求可控、可读、可维护。对 AI Agent 来说,最重要的不是“记住一切”,而是把真正能改善下一次执行的经验,沉淀到团队可以看见、可以修改、可以验证的地方。