数据截至 (上游 commit ae57a2357745)
AgenticSeek — 架构与原理
30 秒导读: AgenticSeek 是一个全本地运行的多专家 AI 助手(自我定位为 Manus 的本地替代品)。你说一句话,它先用两个小型文本分类器判断「该谁接、多难」,再交给会写代码 / 找文件 / 爬网 / 做规划的专家 agent。工具调用不走 function calling,而是认模型回复里的 markdown 代码块。
1. 这是什么(零基础也能懂)
一句话定义: AgenticSeek 是一个跑在你自己机器上的 AI 助手,能自主上网、写并运行代码、翻你指定目录里的文件,还能把一个大任务拆成多步依次做完。
解决什么问题、给谁用
想象你想要这样一件事:
「上网查一下 2025 年最好的几个免费天气 API,挑一个,然后在我的工作目录里写一个 Python 小程序把今天的天气打印出来。」
这件事跨了三种能力:上网、判断、写并运行代码。云端助手能做,但你的文件、你的搜索记录、你的对话都要出门。AgenticSeek 的取舍就一句话:宁可用小一点的模型,也要全部留在本机。
它面向的人群很具体:
- 手上有一块能跑 14B~70B 本地模型的显卡;
- 不愿意把代码库、文件、搜索词交给云端 API;
- 能接受「本地小模型比 GPT 笨一些」这个代价。
它能做什么
| 能力 | 具体是什么 |
|---|---|
| 自主上网 | 起一个真实 Chrome,搜索、逐页阅读、记笔记、甚至填表登录 |
| 写并运行代码 | Python / Bash / C / Go / Java,写完直接执行,看到报错自己改 |
| 操作文件 | 在你指定的工作目录里递归找文件、读内容(含 PDF) |
| 任务规划 | 复杂请求拆成多步 JSON 计划,逐步调度不同 agent |
| 语音交互 | 本地 TTS(kokoro)+ 本地 STT(Vosk),带回声过滤 |
| 两种界面 | 终端 CLI(cli.py)或 React 网页 + FastAPI 后端(api.py) |
用起来什么样
最直观的是 CLI 模式。下面是一次真实交互的形状(示意,非源码输出):
$ uv run cli.py
Initializing...
Loading zero-shot pipeline...
Loading LLM router model...
AgenticSeek is ready.
➤➤➤ 帮我在工作目录里写个 Python 脚本,打印当前目录下所有 .py 文件
Selected agent: coder (roles: code)
Thinking...
────────────────────────────────────────
import os
for f in os.listdir('.'):
if f.endswith('.py'):
print(f)
────────────────────────────────────────
Executing 1 python blocks...
[success] Execution success, code output:
api.py
cli.py
注意两件事,它们是理解整个项目的钥匙:
Selected agent: coder—— 路由发生在调用 LLM 之前,不是模型自己选的。- 模型只是写了一段普通的 markdown 代码块,系统自己把它抠出来跑掉了。
一句话直觉
把 AgenticSeek 想成一个小型电话总机 + 一队专家:
- 总机(路由器)是两个廉价的文本分类器,几十毫秒就判断出「这通电话该转给谁」;
- 专家(各 agent)拿到活之后,用同一套办法干活——说话时顺手写代码块,系统替他按下回车。
总机不用 LLM,是因为本地小模型做元决策(「我该派谁」)非常不稳;把这件事交给判别式小模型,确定、便宜、可控。
2. 顶层全景(它大概怎么转)
2.1 主链路
怎么读这张图:从上往下就是一次请求的生命周期,右侧是每一层用到的关键文件。
用户输入(键盘 / 麦克风 / 网页 POST /query)
|
v
┌──────────────┐
│ Interaction │ 统一入口:拿输入、调路由、说结果 sources/interaction.py
└──────┬───────┘
v
┌──────────────┐
│ AgentRouter │ ① 检语种→翻英文 ② 估难度 ③ 投票选人 sources/router.py
└──────┬───────┘
| 难度 HIGH ──────────────┐
v v
┌──────────────┐ ┌──────────────┐
│ 单个专家 Agent│ │ PlannerAgent │ 拆成 JSON 计划,逐步调度
└──────┬───────┘ └──────┬───────┘
| |(内部再调上面那些 Agent)
v v
┌──────────────────────────────────────┐
│ LLM 循环:问模型 → 抠出代码块 → 执行 │ sources/agents/agent.py
│ → 反馈回灌 memory → 再问 │ sources/tools/*.py
└──────────────────────────────────────┘
|
v
最终答复(文字 + 每个代码块的执行结果)
2.2 部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
Interaction | 会话总控:读输入、调路由、保存/恢复会话、朗读答案 | sources/interaction.py |
AgentRouter | 不用 LLM,用两个分类器决定「谁接 活、多难」 | sources/router.py |
Agent(基类) | 统一的「问 LLM → 抠代码块 → 执行 → 回灌」骨架 | sources/agents/agent.py |
CasualAgent | 闲聊,不带任何工具 | sources/agents/casual_agent.py |
CoderAgent | 写并执行 Python/Bash/C/Go/Java | sources/agents/code_agent.py |
FileAgent | 找文件、读文件、跑 shell | sources/agents/file_agent.py |
BrowserAgent | 搜索 + 逐页导航 + 记笔记 + 填表 | sources/agents/browser_agent.py |
PlannerAgent | 拆任务、调度上面四个、每步之后重规划 | sources/agents/planner_agent.py |
Tools(基类) | markdown 代码块的解析器 + 工作区路径守卫 | sources/tools/tools.py |
Browser | Selenium 驱动:隐身、转 Markdown、填表、截图 | sources/browser.py |
Provider | 14 个 LLM 后端的统一 dispatch 表 | sources/llm_provider.py |
Memory | 对话历史 + 落盘 + 实验性摘要压缩 | sources/memory.py |
| workspace 工具 | 工作区 / 运行时目录解析与越权拦截 | sources/workspace.py |
2.3 主线走一遍(高层,不进代码)
以「查天气 API 并写个脚本」为例:
Interaction.get_user()拿到这句话。AgentRouter.select_agent()先用 langid 检出语种,必要时用 MarianMT 翻成英文;再估复杂度——这句话跨了「查网 + 写码」,被判HIGH,直接交给PlannerAgent,投票环节都不走。PlannerAgent.make_plan()让 LLM 输出一段 ```json 计划:任务 1 交 Web、任务 2 交 Coder,并声明任务 2 需要任务 1 的产出。- 逐步执行:
BrowserAgent起真 Chrome 搜索、读页、记笔记;CoderAgent拿到笔记,写 Python 代码块。 - 执行与自愈:代码块被
Tools.load_exec_block()抠出来,丢进子进程跑;报错就把 stderr 当成「用户消息」推回 memory,让模型重写,最多 5 次。 - 每步之后重规划:
PlannerAgent.update_plan()把这一步的结果和成败喂回 LLM,问它「计划要不要改」,模型答NO_UPDATE就照原计划走。 - 最后由
Interaction打印/朗读答案。
3. 阅读地图(建议顺序)
按「由浅入深」排的。只想抓精华的话,读 01 和 02 就够了——那是这个项目最有辨识度的两处设计。
| 顺序 | 章节 | 讲什么 | 为什么值得读 |
|---|---|---|---|
| 1 | 01-routing.md | 分类器投票路由 + 复杂度分级 | 「不用 LLM 做路由」的完整可运行样本 |
| 2 | 02-blocks-and-execution.md | 代码块协议、执行循环、五种解释器 | 没有 function calling 时,工具调用还能怎么做 |
| 3 | 03-planner.md | JSON 计划、依赖注入、逐步重规划 | 弱模型上怎么撑起 divide-and-conquer |
| 4 | 04-browser.md | 隐身浏览器、页面转 Markdown、笔记式导航 | 纯文本驱动(无视觉)的浏览器 agent 范本 |
| 5 | 05-llm-and-memory.md | 多后端 dispatch、记忆压缩、reasoning 抽取 | 本地/云端后端抽象的最小实现 |
| 6 | 06-sandbox-and-safety.md | 工作区隔离、命令黑名单、API token | 这套系统真正的信任边界在哪 |
4. 巧妙之处(可以带走的技术)
4.1 把「选谁干活」从 LLM 手里拿走
绝大多数 agent 框架让 LLM 自己决定调哪个工具。AgenticSeek 反过来:路由是判别任务,就用判别模型。
AgentRouter 里跑的是 facebook/bart-large-mnli(零样本分类)加一个 AdaptiveClassifier(现场 few-shot 学习),两者各出一个标签+置信度,归一化后比高低(sources/router.py:370-390,router_vote)。代价是几百毫秒的 CPU 推理,换来的是不受模型胡言乱语影响的确定性分发。
4.2 用 markdown 代码块当工具协议
本地小模型输出严格 JSON 的成功率不高,但写代码块是它们被训练得最熟的动作。于是每个工具认领一个围栏标签(python / bash / json / web_search / file_finder),Tools.load_exec_block() 直接扫文本(sources/tools/tools.py:150-200)。
附赠两个小设计:围栏行写成 python:main.py 就顺便存盘;块前有缩进时会按缩进量整体反缩进,免得模型把代码嵌在列表里就跑不了了。
4.3 把系统反馈伪装成用户消息
执行失败后,Agent.execute_modules() 把报错以 role='user' 推回 memory(sources/agents/agent.py:280)。这样对模型来说,「编译器骂你了」和「用户骂你了」是同一件事——不需要额外的 tool-result 角色,任何 chat 接口都吃得下。
4.4 网页 → Markdown,而不是网页 → 截图
Browser.get_text() 先剥掉 script/style,用 markdownify 转成 Markdown,再逐行做「这是不是一句人话」的判定(is_sentence:含数字,或词数≥5 且有标点/够长),把导航栏碎词滤掉,最后硬截到 32768 字符(sources/browser.py:390-418)。整条链路不需要视觉模型。
4.5 语音的回声过滤
开着音箱做语音助手,麦克风会听见自己刚说的话。sources/echo_filter.py 的办法很朴素但有效:双向找 3 个连续词的公共子串,命中就判为回声丢弃(is_echo)。
5. 边界与局限(诚实)
5.1 沙箱是「目录级」的,不是「进程级」的
- 文件路径有真守卫:
resolve_workspace_path()用realpath+commonpath拦截../越权(sources/workspace.py:71-93),FileFinder的每次读文件都过这道关。 - 但代码执行没有。Python 走
subprocess.run([sys.executable, "-c", code]),Bash 走subprocess.Popen(..., shell=True)——只是cwd设成工作区,进程本身能读写整台机器(sources/tools/PyInterpreter.py:60-66、sources/tools/BashInterpreter.py:57-64)。 - 危险命令黑名单(
sources/tools/safety.py)只在safe_mode=True时生效,而config.ini里默认False。
真正的隔离靠 Docker:docker-compose.yml 只把 ${WORK_DIR} 挂进容器,后端端口只发布在 127.0.0.1。在宿主机直接 uv run cli.py 就没有这层保护。 详见 06-sandbox-and-safety.md。
5.2 路由标签和实际 agent 对不齐
投票的两侧用的是两套不同的标签:
- BART 那侧的候 选标签是运行时 agent 的
role:talk/code/files/web/planification(sources/router.py:455)。 AdaptiveClassifier那侧的标签来自手写 few-shot,含mcp(5 条)和一条拼错的coding(sources/router.py:209)。
如果自适应分类器返回 mcp 或 coding 并赢下投票,select_agent 的 for 循环找不到匹配的 agent,返回 None,整轮请求失败(sources/router.py:464-471)。MCP agent 在 cli.py:52-54 是注释掉的,api.py 里根本没注册。
5.3 MCP agent 尚未可用
源码开头直书 “MCP agent is an active work in progress, not functional yet”(sources/agents/mcp_agent.py:9),且没有 MCP_FINDER_API_KEY 时自己关闭。
5.4 coder agent 的循环有个反直觉的分支
CoderAgent.process() 里,bash 块执行成功也不会跳出循环——判断条件是 exec_success and self.get_last_tool_type() != "bash"(sources/agents/code_agent.py:76)。设计意图看起来是让模型能连续下多条 shell 命令,但副作用是:一 串顺利的 bash 操作跑满 5 轮后,函数会返回 “I'm sorry, I couldn't find a solution to your problem.”(sources/agents/code_agent.py:83-84),并且中途每轮都会打印 “Execution failure”。
5.5 coder 的 prompt 教了一种解析不出来的 file_finder 写法
prompts/base/coder_agent.txt 教模型这样找文件:围栏里直接写文件名(或用 ```file_finder:read)。但 FileFinder.execute() 取参数靠 get_parameter_value(block, "name"),即必须有 name=xxx 这一行(sources/tools/fileFinder.py:132、sources/tools/tools.py:127-140)。裸文件名会得到 “Error: No filename provided”。只有 prompts/base/file_agent.txt 教的 name=toto.py 形式能正常工作。
5.6 记忆压缩是实验性的,而且默认关
所有 agent 构造 Memory 时都传 memory_compression=False。即使打开,上下文长度是从模型名里正则抠一个「数字+b」再按幂律估算(sources/memory.py:47-68,get_ideal_ctx),模型名不含数字(如 gpt-4o)时直接返回 None,压缩与截断全部跳过。
5.7 一次只能处理一个请求
api.py 用一个模块级 is_generating 布尔量挡并发,第二个请求直接 429(api.py:248-250)。全局共享同一个 Interaction 单例,没有会话隔离。
6. 横向对比(同 shelf 兄弟)
| 维度 | AgenticSeek | 常见做法 |
|---|---|---|
| 谁决定调哪个 agent | 专用文本分类器投票(BART + AdaptiveClassifier) | 由 LLM 自己在 prompt 里决定 |
| 工具调用协议 | markdown 代码围栏 + 标签 | OpenAI function calling / JSON schema |
| 任务分解 | 一次生成完整 JSON 计划,每步之后重问一次要不要改 | 一次性计划,或纯 ReAct 逐步决策 |
| 浏览器感知 | 纯文本:HTML → Markdown → 句子过滤 | 截图 + 视觉模型,或 DOM 可访问性树 |
| 部署取向 | 本地优先,云端 API 是可选降级 | 云端 API 优先 |
| 多语言 | 路由前用 Helsinki-NLP MarianMT 翻成英文 | 少见 |
延伸阅读(同 shelf):
- 浏览器方向的兄弟项目:browser-use-web-ui、browseros、agent-s
- 自主任务分解方向:autogpt、babyagi
- 多 agent 协作方向:camel、agency-swarm
7. 代码地图(导航索引)
| 主题 | 文件 | 关键符号 |
|---|---|---|
| CLI 入口 + 主循环 | cli.py | main |
| HTTP 后端 + 系统装配 | api.py | initialize_system、process_query、get_latest_answer |
| 会话总控 | sources/interaction.py | Interaction.think、Interaction.get_user、transcription_job |
| 路由 | sources/router.py | AgentRouter.select_agent、router_vote、estimate_complexity、learn_few_shots_tasks |
| agent 基类与执行循环 | sources/agents/agent.py | Agent.execute_modules、Agent.remove_blocks、Agent.sync_llm_request |
| 工具基类与块解析 | sources/tools/tools.py | Tools.load_exec_block、Tools.save_block、Tools.resolve_path |
| 代码执行 | sources/tools/PyInterpreter.py、BashInterpreter.py | PyInterpreter.execute、refuse_interactive_code、BashInterpreter.execute |
| 规划 | sources/agents/planner_agent.py | PlannerAgent.make_plan、update_plan、start_agent_process |
| 浏览器 agent | sources/agents/browser_agent.py | BrowserAgent.process、make_navigation_prompt、parse_answer |
| 浏览器驱动 | sources/browser.py | Browser.get_text、go_to、fill_form、apply_web_safety |
| LLM 后端 | sources/llm_provider.py | Provider.respond、Provider.available_providers |
| 记忆 | sources/memory.py | Memory.push、Memory.compress、get_ideal_ctx |
| 工作区与运行时目录 | sources/workspace.py | resolve_workspace_path、get_work_dir、runtime_subdir |
| 命令黑名单 | sources/tools/safety.py | is_unsafe、unsafe_commands_unix |
| API 鉴权 | sources/api_auth.py | require_api_token |
| 系统提示词 | prompts/base/*.txt | coder_agent.txt、planner_agent.txt、file_agent.txt |