最近开源圈又冒出一个 Agent 框架——OpenJarvis,出自斯坦福 Scaling Intelligence Lab 和 Hazy Research(就是 Christopher Ré 那个组),2026 年 3 月 12 日以 Apache 2.0 协议放出,作者列表里还挂着 Azalia Mirhoseini。和满天飞的 Agent 框架不同,它的口号是 Personal AI, On Personal Devices,目标很直白:让个人 AI Agent 默认跑在你自己的设备上,只在真正必要时才调用云端。

支撑这个口号的是它们之前的 Intelligence Per Watt 研究——本地语言模型已经能处理约 88.7% 的单轮对话/推理查询,且 2023 到 2025 年"每瓦智能"提升了 5.3 倍。一句话:模型和硬件都准备好了,缺的是把本地优先落地的软件栈。OpenJarvis 想做的,用它自己的话说,是"本地 AI 时代的 PyTorch"。

这篇文章咱不复述官方介绍,而是把仓库 clone 下来,从源码角度拆它的几个核心设计:可插拔注册表、trace 驱动的本地/云路由闭环、把能耗写进数据模型的遥测层、四种 Agent 循环原型,以及它的 skill 机制与 Hermes 的本质区别。仓库整体是 Python(82.7%)+ Rust(8.7%)+ TypeScript(7.3%),核心逻辑在 src/openjarvis/

一、五原语 + 一个注册表:可插拔的全部秘密

先纠正一个流传的说法。外面文章爱说"五原语",README 又说"三个核心思想",其实对着代码看,真正的骨架是五个可插拔抽象 + 八条设计原则。而且它自己文档里都没完全统一:架构文档一处写 Intelligence / Engine / Agentic Logic / Memory / Learning,另一处写 Intelligence / Engine / Agents / Tools / Learning。对照 core/registry.py 的实际注册表,真相是 Memory 并非独立原语,而是挂在 Tools 之下的一种后端。所以规范的五原语是:

  • Intelligence:模型目录,记录模型能力、参数量、定价(ModelRegistry)
  • Engine:推理后端,抽象基类 InferenceEngine,实现有 Ollama / vLLM / SGLang / llama.cpp / Cloud
  • Agent:推理循环,抽象基类 BaseAgent
  • Tools(含 Memory):工具与记忆后端,BaseTool / MemoryBackend(SQLite / FAISS / ColBERT / BM25 / Hybrid)
  • Learning:路由策略,抽象基类 RouterPolicy(Heuristic / Learned / GRPO),横切其余四者

让"一切可插拔"成立的,是一个泛型注册表加装饰器。核心就一个类 RegistryBase[T],精髓在按子类名隔离存储:

class RegistryBase(Generic[T]):
    @classmethod
    def _entries(cls) -> Dict[str, T]:
        attr_name = f"_registry_entries_{cls.__name__}"   # 关键:按子类名隔离
        storage = getattr(cls, attr_name, None)
        if storage is None:
            storage = {}
            setattr(cls, attr_name, storage)
        return storage

    @classmethod
    def register(cls, key: str):
        def decorator(entry):
            if key in cls._entries():
                raise ValueError(...)        # 重复注册直接报错
            cls._entries()[key] = entry
            return entry
        return decorator

f"_registry_entries_{cls.__name__}" 这一行把每个子类的存储隔离开,于是 EngineRegistryAgentRegistryRouterPolicyRegistry 各自一套字典,注册时绝不串台。加一个新引擎只需要:

@EngineRegistry.register("my-engine")
class MyEngine(InferenceEngine):
    def generate(self, messages, *, model, **kwargs): ...

单文件改动,没有中央工厂、没有 YAML、不用改任何已有代码,import 即注册。整个仓库有十几个这样的注册表(连后面提到的挖矿模块都有一个 MinerRegistry),全部共享这一套机制。这就是设计原则里"Pluggable Everything"在实现层面的全部内容。

二、Learning 闭环:本地还是云,是"学"出来的

这是 OpenJarvis 区别于 LangChain / CrewAI 的真正内核——别家默认云推理,它把"用哪个模型"做成一个可学习的策略。拆成四个零件。

① 复杂度评分器(learning/routing/complexity.py):纯正则,零模型开销,给 query 打 0~1 分。

score = 0.0
score += 0.20 * length_score            # 长度信号占 20%
if has_code:  domain_score = 0.7        # 代码/数学信号占 25%
# reasoning / multi-step / 问号数 / 子任务数 ... 加权汇总

分数再映射到 token 预算档位(trivial=1024 一路到 very_complex=16384),思考型模型(qwen3.5 / r1 / o1 等)预算翻倍留 CoT 空间。整个过程不调用任何 LLM,所以路由决策本身几乎不耗算力。

② 启发式路由器(learning/routing/router.py):六条规则按序短路。

def select_model(self, context):
    if context.urgency > 0.8:           # 规则5:急 → 最小模型(覆盖一切)
        return _smallest_model(available)
    if context.has_code:                # 规则1:代码 → 找名字带 code/coder 的
        return _find_model_by_tag(available, "code") or _largest_model(...)
    if context.has_math:                # 规则2:数学 → 最大模型
        return _largest_model(available)
    if context.complexity_score < 0.20: # 规则3:简单 → 小模型(省电)
        return _smallest_model(available)
    if context.complexity_score >= 0.55 or context.has_reasoning:  # 规则4:难 → 大模型
        return _largest_model(available)
    return self._default                # 规则6:兜底

注意规则 5 把紧急度凌驾于一切之上——宁可用小模型快速本地出结果,也不甩给云。这正是"本地优先"在代码里的体现。

③ 学习型路由器(learning/routing/learned_router.py):真正"自我改进"的部分。它不靠手写规则,而是从历史 trace 里学 query_class → 最佳模型 的映射,并且带一个置信门槛——样本不够就退回兜底。

def select_model(self, context):
    query_class = classify_query(context.query)
    if (query_class in self._policy_map
            and self._confidence.get(query_class, 0) >= self.min_samples):  # 门槛默认 5 条样本
        return self._policy_map[query_class]
    return self._default

策略表怎么更新?把所有 trace 按 query_class 分组,每个模型算一个复合分,取最高分写进策略表:

def composite_score(self):
    sr = self.successes / self.count               # 成功率
    fb = self.feedback_sum / self.feedback_count   # 用户反馈
    return 0.6 * sr + 0.4 * fb                      # 6:4 加权

④ 奖励函数(learning/routing/heuristic_reward.py):这里能耗/成本意识直接写进了公式。

latency_score    = max(0, 1 - latency / max_latency)
cost_score       = max(0, 1 - cost / max_cost)
efficiency_score = completion_tokens / total_tokens
reward = 0.4*latency_score + 0.3*cost_score + 0.3*efficiency_score

延迟 0.4、成本 0.3、token 效率 0.3——把"快、省钱、不啰嗦"量化成了可优化的标量。

四个零件接起来就是一个闭环:每次推理 → 记一条 trace → 分析成败与反馈 → 更新策略表 → 下一条同类 query 走更优模型。这就是它把"本地优先"从口号变成可优化系统的方式。

三、Intelligence-per-Watt:能耗被写进了数据模型

这是 OpenJarvis 最不一样的地方。别的框架顶多记延迟,它把能耗当一等公民。每次推理自动落库一条 TelemetryRecord,字段里直接有焦耳和瓦特:

@dataclass(slots=True)
class TelemetryRecord:
    latency_seconds: float
    ttft: float              # 首 token 延迟
    cost_usd: float
    energy_joules: float     # ← 焦耳
    power_watts: float       # ← 瓦特
    ...

更狠的是它针对不同硬件写了独立的能耗采集器:energy_rapl.py(Intel CPU)、energy_nvidia.pyenergy_amd.pyenergy_apple.py。NVIDIA 那个不是估算,是直接读硬件计数器——Volta 以上的卡用 nvmlDeviceGetTotalEnergyConsumption() 读起止毫焦,差值除以 1000 就是焦耳;老卡才退回功率轮询做梯形积分。

聚合器(telemetry/aggregator.py)算出来的指标才是论文标题的真身:avg_tokens_per_joule(每焦耳产出多少 token)、avg_energy_per_output_token_joulesavg_throughput_per_watt,甚至把 prefill 和 decode 两个阶段的能耗分开统计。换句话说,"哪个模型在你这台机器上每瓦最划算"是它能直接 SQL 查出来的数字。这也是它"研究平台"属性的核心——一切都要能 benchmark。

顺带一提,读代码时还翻到一个意外模块 mining/MinerRegistry,它在对接一条叫 Pearl 的 PoUW(Proof-of-Useful-Work)区块链,让你本地跑推理的同时"挖矿"。验证端是纯 Rust + plonky2 的 STARK 证明,硬件中立,只认数学对不对。这块还在早期,算是个挺"斯坦福"的彩蛋,这里不展开。

四、八个 Agent,其实是四种循环原型

agents/ 是仓库最大的模块(约 2 万行),但八个内置 Agent 拆开看本质只有几种推理循环,共享同一个 BaseAgent 抽象:

Agent 循环范式 默认最大轮数 工具调用方式
simple 单轮 1
native_react Thought→Action→Observation 10 正则解析自由文本
native_openhands CodeAct(写+跑 Python) 3 多格式兼容
orchestrator 多轮自动选工具 原生 OpenAI tool-calling
operative / monitor_operative 常驻/持续 20 原生 tool-calling
claude_code / opencode / openhands 子 Agent 委派 spawn 外部 agent

这里有个贯穿全局的工程取舍:ReAct 和 CodeAct 用正则解析自由文本(因为本地小模型常常不支持原生 tool-calling),而 orchestrator / operative 走 OpenAI 原生 tool-calling(模型够强时)。这种"双轨"设计正是它能在弱本地模型上也跑起来的原因。

ReAct 的 run() 就是一个朴素 for 循环:生成 → 解析 → 判断终止 → 执行工具 → 把 Observation 塞回上下文。

for _turn in range(self._max_turns):
    if self._loop_guard:
        messages = self._loop_guard.compress_context(messages)   # 防上下文爆炸
    result = self._generate(messages)
    parsed = self._parse_response(result["content"])

    if parsed["final_answer"]:        # 出现 Final Answer → 返回
        return AgentResult(content=parsed["final_answer"], ...)
    if not parsed["action"]:          # 没有 Action → 当成最终回答
        return AgentResult(content=content, ...)

    tool_call = ToolCall(name=parsed["action"], arguments=parsed["action_input"])
    tool_result = self._executor.execute(tool_call)
    observation = f"Observation: {tool_result.content}"
    messages.append(Message(role=Role.USER, content=observation))   # 喂回模型

CodeAct(native_openhands.py)真正值得抄的不是"会跑 Python",而是它的 _extract_tool_call() 同时兼容三种模型输出习惯——因为不同本地模型吐 tool-call 的格式五花八门:

# 格式1:Action / Action Input(标准)
action_match = re.search(r"Action:\s*(.+)", text, re.IGNORECASE)
# 格式2:<tool_call>name $key=value</tool_call>(XML 风格)
#   还分别兼容 $key=value、<key>value</key>、甚至 GLM 模型爱用的 key: value
# 代码块直接执行
match = re.search(r"```python\n(.*?)```", text, re.DOTALL)

这是"本地优先"被迫长出来的肌肉:云端 API 格式统一,本地一堆开源模型各说各话,框架只能在解析层把脏活全包了。

常驻模式 operative 的核心在于跨会话状态持久化,状态和会话历史都落到 memory backend,用 operator:{id}:state 这种 key,还带一个兜底——即便模型自己忘了存状态,框架也会自动把回复摘要存下来:

def _auto_persist_state(self, content):     # agent 没显式存就自动存摘要
    summary = content[:1000]
    self._memory_backend.store(f"operator:{self._operator_id}:state", summary)

最后是一个容易被忽略但很实用的防呆层 loop_guard.py,用两招防 agent 卡死:对 (工具名, 参数) 做 SHA-256 哈希计数,同一调用重复超过 3 次直接拦截;再用滑动窗口检测 A-B-A-B 这种来回横跳。对要长时间无人值守跑的端侧 agent,这层几乎是必需品。

max_identical_calls = 3        # 同一 (tool, args) 重复超 3 次 → 拦
ping_pong_window = 6           # 检测 A-B-A-B 来回横跳
max_context_messages = 100     # 上下文超 100 条 → 压缩

五、Skill 机制:和 Hermes 的本质区别

Hermes(Nous Research 出品)是目前社区最火的 Agent,两者经常被拿来比。最有意思的是,我从代码里挖到一个关键差异——它俩号称的"skill 自我进化"根本不是一回事。

先看 OpenJarvis 的 skill 数据模型。它不只是一段文档,而是一条可执行流水线(skills/types.py):

@dataclass(slots=True)
class SkillStep:
    tool_name: str = ""              # 调用某个工具
    skill_name: str = ""             # 或者调用另一个 skill(可组合)
    arguments_template: str = "{}"   # Jinja 风格模板
    output_key: str = ""             # 结果存到上下文哪个 key

@dataclass(slots=True)
class SkillManifest:
    name: str
    signature: str = ""              # Base64 Ed25519 签名(可验真伪)
    markdown_content: str = ""       # 从 SKILL.md 加载的正文

两个细节:skill 可以用 skill_name 调用另一个 skill(组合性);每个 skill 带 Ed25519 签名,因为要从外部仓库导入,得能验证没被篡改。README 那句"每个 skill 都是一个 tool"在代码里就是 SkillTool 适配器,tool_id = f"skill_{name}",还能从模板里的 {placeholder} 自动反推出参数表。于是 agent 在工具目录里看到的 skill 和普通工具长得一模一样,按需调用。

skills/sources/ 下有四个解析器,其中 HermesResolver 直接 git clone NousResearch/hermes-agent,遍历它的 skills/<category>/<skill>/SKILL.md 目录。能互导的前提是双方都遵循 agentskills.io 标准——所以 jarvis skill install hermes:arxiv 就能把 Hermes 的技能拿过来,反过来也行。

而最关键的区别在自我进化的实现哲学,两者几乎相反:

Hermes OpenJarvis
触发方式 完成复杂任务(5+ 工具调用)后自主写新 skill 攒够 trace(默认 ≥20 条)后批量优化已有 skill
进化对象 生成全新技能文档 优化已有技能的描述 + few-shot 示例
进化时机 在线、运行中 离线、批处理
进化引擎 LLM 自己写 DSPy / GEPA 优化器
是否改原文件 直接写/改 不改原文件,写 sidecar overlay
是否度量效果 主要靠 agent 判断 benchmark 量化 impact

learning/agents/skill_optimizer.py 就明白了:它把 trace 按 skill 名分桶,够 20 条就跑 DSPy 或 GEPA,把优化结果写成一个叠加文件,而不动原版。

class SkillOptimizer:
    def __init__(self, *, min_traces_per_skill=20, optimizer="dspy"):  # 或 "gepa"
        ...
    def optimize(self, trace_store, ...):
        buckets = self._bucket_traces_by_skill(traces)   # 按 skill 分桶
        for skill_name, skill_traces in buckets.items():
            if len(skill_traces) < self._min_traces:
                continue                                   # 样本不够就跳过
            output = self._run_dspy(...)  # 或 _run_gepa(...)
            # 结果写到 ~/.openjarvis/learning/skills/<name>/optimized.toml

一句话总结这个区别:Hermes 是"写新技能"的生成式进化,OpenJarvis 是"调优老技能"的优化式进化。前者更野蛮生长、更像一个有自主性的个体;后者更克制、可度量、可回滚(overlay 不动原文件),骨子里是斯坦福那套"一切都要能 benchmark"的研究范式。

写在最后

把这几块拼起来,OpenJarvis 的设计意图就很清楚了:它不是又一个"调用云 API 的 Agent 编排器",而是一套以本地执行为默认、把能效当成一等约束、用 trace 反馈持续优化路由与技能的研究型基础设施。

如果你做端侧或本地 AI,有三条工程范式值得直接借鉴,而且都不依赖它的具体实现:一是遥测原生,把能耗、tokens-per-joule 做进核心数据模型而非事后埋点,且遥测失败绝不阻塞主流程;二是硬件感知的默认值,启动时探测 GPU/CPU/RAM 自动写出最优后端配置;三是复杂度路由即省电策略,用一个几乎零成本的分类器换取大幅能耗节省。

它和 Hermes 的关系也不是非此即彼——一个是社区驱动、自我进化的实用主力,一个是可度量、可复现的研究平台,而且通过 agentskills.io 标准还能互相导入技能。对想理解"本地优先 AI Agent 到底该怎么搭"的人来说,这份源码本身就是很好的教材。


项目地址与参考

  • GitHub:github.com/open-jarvis/OpenJarvis(Apache 2.0)
  • 文档:open-jarvis.github.io/OpenJarvis
  • 项目主页:scalingintelligence.stanford.edu/blogs/openjarvis
  • 论文:arXiv 2605.17172
  • Intelligence Per Watt:intelligence-per-watt.ai
  • 对比项 Hermes Agent:github.com/NousResearch/hermes-agent

本文基于公开仓库源码阅读整理,代码片段为说明性节选,以仓库最新版本为准。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐