OpenLum.Console 項目說明
這個項目是參考OpenClaw的CSharp版控制臺智能體助手,Aot發布后主體程序7mb大小,另外的Skills文件夾目前自帶了瀏覽器操作、office文件讀取等基礎工具。
用戶可自行動態擴展Skills(描述提供地址及操作方式后,即可學會各種技能,比如登錄到公司網絡報銷發票、請假考勤等。注意:部分網站的DOM可能不易交互導致失敗)
基于 .NET 的通用智能體 Shell,原生 AOT 發布、零第三方依賴。
面向本地/內網部署,支持 OpenAI API 兼容的各類模型(DeepSeek、Ollama、OpenAI 等)。
瀏覽器搜索信息獲取操作

自帶規劃拉取信息、創建工具、完成任務(pdf文檔生成)

技能的按需加載示例

全部開源免費,新朋友可以關注公眾號“螢火初芒”回復"OpenLum"獲取倉庫地址,有問題可留言或私信作者。讓我們一起探索 AI 助手的無限可能!直接用的的話可在release里下載解壓后運行即可,無需繁瑣安裝與配置。
skill中的工具exe可查看之前的文章,有相關功能的aot發布,實現快速命令行啟動。
一、項目定位與特點
1.1 一句話定位
OpenLum.Console 是一個控制臺可交互的通用 AI 智能體 Shell:
你輸入自然語言,模型調用工具(讀文件、執行命令、搜索記憶、 spawning 子智能體等),在既定策略下完成復雜任務。
1.2 核心特點
| 特點 | 說明 |
|---|
| Native AOT | 支持 PublishAot=true,dotnet publish -r win-x64 產出單一可執行文件,冷啟動快、體積可控 |
| 零 NuGet 依賴 | 不引用任何 PackageReference,僅用 BCL + System.Text.Json,便于內網、離線環境部署 |
| OpenAI API 兼容 | 通過 baseUrl 支持 DeepSeek、Ollama、本地/代理 OpenAI 等一切兼容 Chat Completions 的接口 |
| 工具策略可配置 | 基于 profile(minimal / coding / local / full)+ allow/deny 精細控制可用工具 |
| Skill 機制 | 從 skills/*/SKILL.md 自動發現技能,模型按需 read 加載,再用 exec 調用技能 exe |
| 會話壓縮 | 長對話時自動用模型總結歷史,保留最近 N 條,控制 token 消耗 |
| 時間戳注入 | 用戶輸入前自動加 [Dow YYYY-MM-DD HH:mm +08:00],模型具備“今日”感知 |
這些設計都是為了:在盡量少依賴、少配置的前提下,讓一個本地可執行文件就能跑起完整的 Agent 能力。
單獨的文件發布后不到7Mb。Skills不含瀏覽器工具,壓縮后不到30mb,可自行擴展或直接粘貼Claude的Skills。
二、技術架構與搭建思路
2.1 整體數據流
Program.Main
└── Application.Run()
├── ConfigLoader.Load() → AppConfig
├── ToolRegistry → 注冊 read / write / list_dir / exec / memory_* / sessions_spawn
├── ToolPolicyFilter → 按 profile + allow/deny 過濾工具
├── OpenAIModelProvider → HTTP 調用 Chat Completions
├── SystemPromptBuilder → 構建系統提示(工具列表 + Skills + Workspace)
├── ConsoleSession → 內存會話
├── SessionCompactor? → 可選:超閾值時壓縮歷史
└── AgentLoop → 主循環:user → model → tools → model → ...
用戶輸入經過 TimestampInjection 打上時間戳,送入 AgentLoop;
模型返回的 tool_calls 經 ExecuteToolAsync 執行,結果再回傳模型;
直到模型不再調用工具,返回最終回復。
2.2 核心模塊職責
| 模塊 | 職責 |
|---|
| ConfigLoader | 從 openlum.json / openlum.console.json / appsettings.json 加載配置,使用 JsonDocument 解析,無額外 JSON 庫 |
| ToolRegistry | 工具注冊與按名稱查找;ToolPolicyFilter 包裝后實現策略過濾 |
| SystemPromptBuilder | 拼裝系統提示:日期、工具列表、工作區、Skill 元數據(模型通過 read 按需加載 SKILL.md) |
| AgentLoop | 實現 IAgent,負責多輪 tool-call 循環,最多 50 輪,超限時強制 wrap-up |
| SessionCompactor | 當 MessageCount > maxMessagesBeforeCompact 時,用模型總結舊消息,替換為一條摘要 |
| SkillLoader | 掃描 workspace/skills、AppContext.BaseDirectory/skills 等目錄下 SKILL.md,解析 frontmatter,生成 <available_skills> 片段 |
2.3 接口抽象
核心契約集中在 Interfaces 下,便于替換實現:
public interface ITool
{
string Name { get; }
string Description { get; }
IReadOnlyList<ToolParameter> Parameters { get; }
Task<string> ExecuteAsync(IReadOnlyDictionary<string, object?> args, CancellationToken ct = default);
}
public interface IModelProvider
{
Task<ModelResponse> ChatAsync(
IReadOnlyList<ChatMessage> messages,
IReadOnlyList<ToolDefinition> tools,
IProgress<string>? contentProgress,
CancellationToken ct = default);
}
public interface ISession
{
IReadOnlyList<ChatMessage> Messages { get; }
void Add(ChatMessage msg);
void Clear();
}
這樣設計的好處是:
- 換模型只需實現
IModelProvider; - 換存儲只需實現
ISession; - 新增工具實現
ITool 并注冊即可。
三、智能體設計思路
3.1 AgentLoop 的核心循環
智能體本質是一個 “用戶輸入 → 模型推理 → 工具執行 → 模型推理 → … → 最終回復” 的循環。
AgentLoop.RunAsync 的簡化邏輯如下:
_session.Add(new ChatMessage { Role = MessageRole.User, Content = userPrompt });
for (var turn = 0; turn < maxTurns; turn++)
{
if (_compactor is { } c && _session is ICompactableSession cs)
await c.CompactIfNeededAsync(cs, ct);
var messages = BuildMessages(...);
var toolDefs = _tools.All.Select(t => new ToolDefinition(...)).ToList();
var response = await _model.ChatAsync(messages, toolDefs, contentProgress, ct);
if (response.ToolCalls.Count == 0)
{
_session.AddAssistant(response.Content, null);
return new AgentTurnResult(true, null);
}
_session.AddAssistant(response.Content, response.ToolCalls);
foreach (var tc in response.ToolCalls)
{
var result = await ExecuteToolAsync(tc, ct);
results.Add(result);
}
_session.AddToolResults(results);
}
3.2 工具調用與錯誤處理
- 模型返回的
arguments 可能是空字符串(如 DeepSeek),代碼會規范為 "{}"。 - 未知工具名返回
Error: unknown tool 'xxx',模型有機會調整策略。 ExecTool 在執行前會校驗 skill exe 是否存在,避免 LLM 幻覺調用不存在的文件,并提示“先 read SKILL.md 確認路徑”。
3.3 強制收尾(Force Wrap-Up)
當達到 maxTurns 但模型仍請求 tool_calls 時,不再執行工具,而是向會話中注入一條系統提示:
[System: 本輪工具調用次數已達上限。請根據目前已有的信息,給用戶一個簡潔的總結和回答。不要再調用任何工具。]
然后做一次無工具調用的模型請求,得到總結后返回,避免無限循環。
3.4 子智能體(sessions_spawn)
sessions_spawn 工具會創建一個獨立會話和排除自身的工具集,用同樣的 AgentLoop 跑一個子任務,最終返回子智能體的最后一條 assistant 回復。
這樣可以把復雜任務拆成子任務,隔離上下文,降低主會話長度。
四、工具與 Skill 機制
4.1 內置工具一覽
| 工具 | 作用 | 說明 |
|---|
read | 讀文件 | 支持 workspace 相對路徑、~、skill 目錄;純文本限 200–2000 行;PDF/Office 通過 exec + read-*.exe |
write | 寫文件 | 限制在工作區下 |
list_dir | 列目錄 | PowerShell 風格 |
exec | 執行命令 | PowerShell(Windows)/ sh(非 Windows);支持 stdin、timeout;skill exe 校驗 |
memory_get | 讀記憶 | MEMORY.md / memory/*.md,支持行范圍 |
memory_search | 搜記憶 | 關鍵詞匹配,無向量 |
sessions_spawn | 子智能體 | 獨立會話執行子任務 |
4.2 工具策略(profile)
策略由 ToolProfiles 和 ToolPolicyFilter 實現:
- Profile:
minimal / coding / messaging / local / full,映射到不同的工具/組。 - 組:
group:fs、group:web、group:runtime、group:memory、group:sessions。 - allow / deny:在 profile 基礎上追加或排除工具。
例如 profile: "local" 等價于允許:group:fs + group:web + group:runtime + group:memory。
4.3 Skill 加載與使用
Skill 目錄結構:
skills/
webbrowser/
SKILL.md
browser/openlum-browser.exe
read/
SKILL.md
pdf/read-pdf.exe
docx/read-docx.exe
...
SKILL.md 基礎格式要求
為正確生成元數據并注入到系統提示,每個 SKILL.md 必須滿足:
- YAML frontmatter:以
--- 開頭和結尾,包裹 YAML 塊 name(可選):skill 的顯示名稱;缺省時使用目錄名(如 webbrowser)description(可選):簡短說明,供模型判斷何時加載該 skill;缺省時為 "Skill: {name}"
---
name: webbrowser
description: "瀏覽網頁。與 read skill 風格一致:直接 exec 調用 exe,傳參執行,stdout 為結果。"
---
description 支持帶引號或不帶引號;name 和 description 建議都填寫,以便模型準確選用。
SkillLoader 掃描 skills/*/SKILL.md,解析上述 frontmatter。
系統提示中會注入 <available_skills> 片段,包含 name、description、location。
模型不直接獲得完整 SKILL.md,而是需要時用 read 工具加載,這樣既節省 token,又保證指令是最新的。
系統提示中的指引:
Use the read tool to load a skill's SKILL.md at the listed location when needed.
Before exec with a skill exe: always read that skill's SKILL.md first to get the exact exe path.
4.4 Skill 動態注入邏輯
Skill 的注入發生在應用啟動時,每次 Application.Run() 都會重新掃描目錄并構建系統提示。流程如下:
Application.Run()
└── SystemPromptBuilder.Build(workspaceDir, tools)
└── BuildSkillsSection(workspaceDir)
├── SkillLoader.Load(workspaceDir)
└── SkillLoader.FormatForPrompt(...)
1. 目錄掃描(SkillLoader.Load)
按優先級掃描三個目錄,取并集(同名 skill 以先發現的為準):
| 優先級 | 目錄 |
|---|
| 1 | {workspaceDir}/skills |
| 2 | AppContext.BaseDirectory/skills(exe 同目錄) |
| 3 | {父級目錄}/skills |
對每個目錄下的子目錄 X,若存在 X/SKILL.md,則視為一個 skill。用 Path.GetFileName(sub) 得到默認名,再通過 frontmatter 的 name: 覆蓋。
var dirs = new[] {
Path.Combine(workspaceDir, "skills"),
Path.Combine(AppContext.BaseDirectory, "skills"),
Path.Combine(Path.GetDirectoryName(AppContext.BaseDirectory) ?? ".", "skills")
};
foreach (var dir in dirs)
{
foreach (var sub in Directory.GetDirectories(dir))
{
var skillPath = Path.Combine(sub, "SKILL.md");
if (!File.Exists(skillPath)) continue;
var (desc, parsedName) = ParseFrontmatter(skillPath);
var skillName = !string.IsNullOrWhiteSpace(parsedName) ? parsedName : Path.GetFileName(sub);
results.Add(new SkillEntry(skillName, desc, skillPath));
}
}
2. Frontmatter 解析
僅解析 --- 塊內的 name: 和 description:,用于生成元數據;完整 SKILL.md 不在此階段讀入。
var match = Regex.Match(text, @"^---\s*\r?\n(.*?)\r?\n---", RegexOptions.Singleline);
3. 注入到系統提示
FormatForPrompt 只輸出 name / description / location 三樣,不輸出 SKILL.md 正文,以減少 token 并讓模型按需加載:
<available_skills>
<skill>
<name>webbrowser</name>
<description>瀏覽網頁。與 read skill 風格一致:直接 exec 調用 exe,傳參執行,stdout 為結果。</description>
<location>D:/app/skills/webbrowser/SKILL.md</location>
</skill>
...
</available_skills>
Use the read tool to load a skill's SKILL.md at the listed location when needed.
Before exec with a skill exe: always read that skill's SKILL.md first to get the exact exe path.
路徑中的用戶主目錄會 compact 成 ~,以節省 token。
“動態”的含義:每次啟動應用時都會重新掃描上述目錄。新增 skills/新技能名/SKILL.md 后,重啟即可被自動發現,無需改代碼。
4.5 基于 Skill 學會技能并調用第三方
Skill 的本質是:用 SKILL.md 教會模型如何通過 exec 調用第三方 exe。模型不預訓練技能,而是運行時“按需學習”。
整體流程
用戶任務 → 模型看 <available_skills> 元數據 → 按 description 選 skill
→ read(SKILL.md) 加載完整說明 → 按文檔構造 exec 命令 → 執行 exe → 解析 stdout → 繼續推理
1. 從元數據選技能
模型只看到 name、description、location。例如用戶說“幫我打開 Bing 搜索”,description 里出現“瀏覽網頁”,模型會選 webbrowser,并知道要 read 對應 location 的 SKILL.md。
2. read 加載 SKILL.md
SKILL.md 是給模型看的說明書,一般包含:
- exe 路徑(如
skills/webbrowser/browser/openlum-browser.exe) - 命令格式、子命令、參數
- 示例命令
- 錯誤處理與注意事項
ReadTool 通過 SkillLoader.GetSkillRoots 拿到的 _extraReadRoots,允許讀取 skills 目錄下的文件;讀到 SKILL.md 時還會在控制臺打 [skill] Loaded: webbrowser 日志。
3. exec 調用 exe
模型根據 SKILL.md 構造 PowerShell 命令,例如:
& "skills/webbrowser/browser/openlum-browser.exe" --visible navigate --url "https://cn.bing.com"
ExecTool 在執行前會做一次校驗:若命令中涉及 skills 目錄下的 exe,則檢查該 exe 是否存在;不存在則返回錯誤,并提示“請先 read 該 skill 的 SKILL.md 確認正確的 exe 路徑”,避免模型幻覺出錯誤路徑。
4. 第三方 exe 的契約
Skill 下的 exe 通常遵循統一風格:
- 輸入:命令行參數
- 輸出:stdout 文本或 JSON
- 工作目錄:若 exe 在 skills 下,
ExecTool 會將工作目錄設為 exe 所在目錄,方便加載同目錄的 DLL(如 pdfium)
模型從 stdout 解析結果,再決定下一步(如解析 snapshot 中的 ref,繼續調用 type、click 等)。
示例:webbrowser skill 的典型調用鏈
| 步驟 | 模型動作 | 結果 |
|---|
| 1 | 根據 description 選 webbrowser | - |
| 2 | read(skills/webbrowser/SKILL.md) | 獲得 exe 路徑、navigate/type/click 等命令格式 |
| 3 | exec: openlum-browser.exe --visible navigate --url "https://cn.bing.com" | 瀏覽器打開,返回 snapshot |
| 4 | 從 snapshot 找到搜索框 ref | - |
| 5 | exec: openlum-browser.exe type --ref 2 --text "關鍵詞" --submit | 輸入并搜索 |
| 6 | 解析新頁面 snapshot,提煉答案 | 回復用戶 |
擴展新 Skill 的步驟
- 在
skills/ 下新建目錄,如 my-tool/ - 編寫
my-tool/SKILL.md(含 frontmatter、exe 路徑、命令格式、示例) - 將 exe 放入
my-tool/ 或其子目錄 - 重啟應用,skill 自動被掃描并注入
<available_skills> - 模型在需要時 read SKILL.md,再通過 exec 調用你的 exe
無需修改 Agent 代碼,只需遵循“exe + SKILL.md”的約定即可接入任意第三方能力。
4.6 自建 Skill 詳細指南
是的,用戶只要在 skills 文件夾里新建目錄、寫好 SKILL.md 和可執行程序,重啟應用即可使用。 無需改 Agent 源碼、無需重新編譯。
4.6.1 目錄結構
skills/
你的技能名/ ← 文件夾名會作為默認 skill 名,也可用 frontmatter 的 name 覆蓋
SKILL.md ← 必需:模型按需加載的“說明書”
你的程序.exe ← 或其他可執行文件,python基本等,可放在子目錄,skill文檔要寫調用方式
其他依賴.dll ← 可選,exe 同目錄可被加載
4.6.2 SKILL.md 必需內容
1. Frontmatter(必須在文件開頭)
---
name: 技能顯示名
description: "一句話描述,用于模型判斷何時選用此技能。盡量包含關鍵詞。"
---
name:可選,不寫則用文件夾名description:務必寫好,模型根據它決定是否選用該 skill
2. exe 路徑
用表格或列表明確寫出 exe 的相對路徑(相對應用根目錄或 workspace)。模型會根據這里構造 exec 命令,路徑寫錯會導致調用失敗。
## exe 路徑
| exe | 說明 |
|-----|------|
| skills/我的技能/run.exe | 主程序 |
3. 命令格式與示例
說明子命令、參數、典型用法。模型會按文檔構造命令行。
## 命令
run.exe <action> [選項]
- action: search | fetch | ...
## 示例
```powershell
& "skills/我的技能/run.exe" search --keyword "test"
#
skills/weather/
SKILL.md
weather.exe
**SKILL.md**:
```markdown
---
name: weather
description: "查詢指定城市天氣。調用 weather.exe,傳入城市名,stdout 返回 JSON。"
---
| exe | 說明 |
|-----|------|
| skills/weather/weather.exe | 天氣查詢程序 |
weather.exe --city <城市名>
## 輸出
stdout 為 JSON:`{"city":"北京","temp":15,"desc":"晴"}`
## 示例
```powershell
& "skills/weather/weather.exe"
**weather.exe**:任意語言編寫,只需讀取命令行參數、輸出到 stdout 即可。例如 C
完成后,將 `skills/weather/` 放到應用同級的 `skills` 目錄(或 workspace 下的 `skills`),**重啟 OpenLum.Console**,新 skill 即被掃描并出現在 `<available_skills>` 中。用戶問「北京今天天氣怎么樣」時,模型會先 read 該 SKILL.md,再 exec 調用 weather.exe。
- **exe 路徑**:推薦用 `skills/技能名/xxx.exe` 這種相對路徑,跨機器可移植
- **工作目錄**:ExecTool 會檢測 skills 下的 exe,并將進程工作目錄設為 exe 所在目錄,因此 exe 同目錄的 DLL、配置文件可直接加載
- **讀文件**:若 exe 需要讀 workspace 下的文件,路徑可用相對 workspace 的寫法,因為 exec 的默認工作目錄是 workspace(非 skill exe 時)
| 步驟 | 檢查項 |
|------|--------|
| 1 | 在 `skills/` 下創建子目錄 |
| 2 | 編寫 `SKILL.md`,含 frontmatter(name、description) |
| 3 | 在 SKILL.md 中寫明 exe 的準確路徑 |
| 4 | 在 SKILL.md 中寫明命令格式和示例 |
| 5 | 將 exe 放到 SKILL.md 中聲明的路徑 |
| 6 | 重啟 OpenLum.Console |
| 7 | 用自然語言測試(如「用天氣技能查一下上海天氣」) |
---
采用「目錄掃描 + SKILL.md + exec 調用」這種設計,帶來以下好處:
| 好處 | 說明 |
|------|------|
| **零代碼擴展** | 用戶不需要改 C
| **任意語言實現** | exe 可用 C
| **自然語言即配置** | SKILL.md 是給人看的文檔,也是給模型看的指令。改說明即可改行為,無需改配置格式 |
| **按需加載,省 token** | 系統提示只注入 name、description、location,不注入 SKILL.md 全文。模型只有在需要時才 read 加載,避免把所有技能文檔塞進上下文 |
| **版本與部署解耦** | 技能可單獨更新:替換 exe、改 SKILL.md 即可,不必動主程序。內網環境可以只同步 skills 目錄 |
| **本地優先、可審計** | 技能邏輯在本地 exe 中,行為可審計、可調試。不依賴外部 API 時,可完全離線運行 |
| **統一契約** | 所有 skill 都通過 exec 調用,ExecTool 統一處理超時、stdin、工作目錄、exe 存在性校驗,減少重復邏輯 |
| **易分發** | 把 `skills/` 打成 zip 分享,別人解壓到同目錄即可使用,無需安裝依賴(exe 自帶運行時除外) |
核心思想是:**把 Agent 做成“殼”,把能力做成“可插拔的 skill”**。殼只負責調度(read、exec、會話管理),具體能力由用戶用「SKILL.md + exe」自行擴展,既降低門檻,又保持架構清晰。
---
配置文件按優先級查找:`openlum.json` > `openlum.console.json` > `appsettings.json`。
放在可執行文件同目錄即可。
示例 `openlum.json`:
```json
{
"tools": { "profile": "local", "allow": [], "deny": [] },
"model": {
"provider": "DeepSeek",
"model": "deepseek-chat",
"baseUrl": "https://api.deepseek.com/v1",
"apiKey": "sk-xxx"
},
"compaction": {
"enabled": true,
"maxMessagesBeforeCompact": 30,
"reserveRecent": 10
},
"workspace": ".",
"userTimezone": "Asia/Shanghai"
}
5.2 配置項說明
| 配置塊 | 字段 | 說明 |
|---|
tools | profile | minimal / coding / messaging / local / full |
tools | allow / deny | 工具名或組名列表 |
model | provider | 僅作標識,實際請求由 baseUrl 決定 |
model | model | 模型名,如 deepseek-chat、qwen3:8b |
model | baseUrl | API 地址,如 Ollama http://localhost:11434/v1 |
model | apiKey | 密鑰(當前不從環境變量讀?。?/td> |
compaction | enabled | 是否啟用會話壓縮 |
compaction | maxMessagesBeforeCompact | 超過此條數觸發壓縮 |
compaction | reserveRecent | 壓縮后保留最近 N 條 |
workspace | - | 工作區根目錄,支持環境變量展開 |
userTimezone | - | 時間戳時區,如 Asia/Shanghai |
5.3 構建與運行
dotnet run -p OpenLum.Console
dotnet publish -c Release -r win-x64 -p:PublishAot=true
發布后需將 Skills/** 一并拷貝到運行目錄,項目已通過 CopySkillsToPublish 目標處理。
5.4 REPL 命令
| 命令 | 作用 |
|---|
/help | 顯示幫助 |
/clear | 清空會話 |
/quit | 退出 |
六、代碼示例片段
6.1 時間戳注入
return $"[{dow} {dateTime} {offsetStr}] {message}";
var expandedPath = ExpandPath(path);
if (!IsPathAllowed(fullPath))
return "Error: path is outside workspace or skill directories";
if (TryExtractSkillExePath(command, out var exePath) && !File.Exists(exePath))
{
return $"Error: 技能 exe 不存在: {exePath} 請先 read 該 skill 的 SKILL.md 確認正確的 exe 路徑...";
}
6.4 會話壓縮流程
if (session.MessageCount <= _maxMessagesBeforeCompact) return false;
var toSummarize = session.GetMessagesToCompact(_reserveRecent);
var summary = await SummarizeAsync(toSummarize, ct);
session.CompactWithSummary(_reserveRecent, summary);
轉自https://www.cnblogs.com/luojin765/p/19655952
該文章在 2026/3/2 8:18:13 編輯過