Files
efi-kernel/文档/02-写一个驱动.md
lou 2155f65c76 加"技能执行"示例驱动: 把 skill 真跑一遍 (选模型 / 拼 prompt / 校验输出 / 重试)
- 驱动/技能执行/: oneshot + system 解释器. 读自带 技能/*.json -> 按 tier 选模型 ->
  拼 prompt -> 调 OpenAI 兼容端点 -> 剥 JSON -> 逐条按档校验 -> 落盘 + 写 events
  main 档只锁外层 (plan 是数组 + 每步有 want); small 档锁死 (单值 want + 必须在契约
  清单里 + args 键白名单 + confidence 只许 0/0.5/1)
- 契约清单只读 PG 的 drivers 表拿 (不 import 别的驱动、不翻别人的文件夹);
  也支持 EFI_SKILL_CONTRACTS 显式给一份 (手工跑 / PG 不在时)
- 思考型模型把 max_tokens 吃光时, 把预算翻倍重试一次; 本机端点强制不走代理
- 驱动/Json解码: 补 provides ["Json解码:解析"] -- skill 样例里一直在用这个契约名, 之前没人提供
- 文档同步: README / 文档 02 / 文档 06 / 文档 10 (坑 27~30) / 设计 01 / 设计 05 (补记执行侧)
- .gitignore: 盖住 驱动/*/输出/ (运行时产物不进 git)
2026-09-17 12:39:10 +08:00

10 KiB
Raw Permalink Blame History

02 · 写一个驱动

驱动 = 一个文件夹。内核认不认它只看一件事:根目录有没有 配置.efi.json。 字段表原文在 设计/01-驱动规范.md 第 3 节(那份是规范,这份是动手做)。本机已有五个真样板: 驱动/Json解码oneshot)、驱动/样板常驻resident + 提供服务)、驱动/样例消费器resident + 消费服务)、 驱动/样板技能(带 skill,只列不执行)、驱动/技能执行示例驱动:把 skill 真跑一遍)。

1. 五分钟:一个最小驱动(oneshot)

cd ~/桌面/工作区/内核
mkdir -p 驱动/问好       

驱动/问好/配置.efi.json

{
  "efi": 1,
  "name": "问好",
  "runtime": "python",
  "mode": "oneshot",
  "entry": "问好.py",
  "interpreter": "system",
  "note": "最小驱动: 打印一句话就退"
}

驱动/问好/问好.py

#!/usr/bin/env python3
"""最小驱动: 打印一句就退 (oneshot)."""
import os

print("问好: 你好, 我是驱动", os.getpid(), flush=True)
print("问好: 内核给我的库连接串 EFI_DB =", "有" if os.environ.get("EFI_DB") else "没有", flush=True)

跑起来:

./.venv/bin/python 内核/内核.py 扫描                # 认文件夹 + 9 条校验 + 写注册表
./.venv/bin/python 内核/内核.py 启动 问好           # oneshot 跑完就退 = 正常
./.venv/bin/python 内核/内核.py 状态 问好
./.venv/bin/python 内核/内核.py 日志 问好 -n 20

状态 里会显示 已退出(oneshot 跑完就退是正常态,不是失败),日志 里是它那两行。

2. 先选形态(选错会白写)

mode 什么时候用 内核怎么对待它
oneshot 一次性任务:解码一个文件、跑一趟抓取、生成一份报告 起 → 等它自己退;退出码 0 = 正常;不会被"重拉"
resident 长跑能力:提供契约、守着一个端口、定时干活 起 → 判定为常驻;崩了按 restart 策略决定要不要重拉
runtime 文件夹里放什么 内核怎么起
python .py +(可选).venv/ <解释器> <entry>cwd= 驱动根目录
exec 可执行文件(ELF / 脚本,看 x 位 不看后缀) 直接 exec,内核不找解释器

python 的解释器解析顺序(interpreter 字段):显式路径 → venv.venv/bin/python,不存在回落 system 并记 WARN)→ systempython3)→ 默认 venv本机样板都用 system,因为样板不需要第三方包。

3. 驱动能碰什么、不能碰什么(硬规矩)

驱动能不能写 说明
events ✔ 随便写 汇报唯一通道(source/driver 写自己名字)
calls ✔ 只能插自己的请求 + 回填自己那条的结果 要别人的东西走这里
drivers / driver_state 内核的地盘(否则驱动能去停别人的进程)
commands 引导器的地盘

环境变量:内核对每个驱动注入 EFI_DB(libpq 关键字式连接串)。密钥类信息走 配置.efi.jsonenv 段或环境变量,不要落盘

import os, psycopg2
连接 = psycopg2.connect(os.environ["EFI_DB"])   # 连接串由内核给, 驱动不用知道库在哪
连接.autocommit = True

驱动自己 venv 里没装 psycopg2 也能跑(只是不能汇报/发请求)—— 内核不会因此判它失败。 样板就是这么写的:拿不到 EFI_DB 就老老实实跑,不装成功。

4. 汇报:往 events 表插一行

没有协议、没有 socket、没有消息格式 —— 想说什么就插一行(内核只读表,不解析你的 stdout):

def 汇报(连接, message: str, kind: str = "heartbeat") -> None:
    """kind 约定: start|stop|exit|log|produce|heartbeat|error; level: info|warn|error."""
    try:
        with 连接.cursor() as 游标:
            游标.execute(
                "INSERT INTO events (source, driver, level, kind, message, data)"
                " VALUES (%s, %s, 'info', %s, %s, %s::jsonb)",
                ("我的驱动名", "我的驱动名", kind, message, json.dumps({"pid": os.getpid()}, ensure_ascii=False)),
            )
    except Exception as :          # 汇报失败不该把驱动搞死
        print("汇报失败:", , flush=True)

看别人汇报:内核/内核.py 事件 -n 50,或按驱动过滤(db.读事件(连接, 条数, driver=名))。 别刷太密 —— events 是总线,刷满了有用的事就看不见了(样板 30s 一条心跳)。

5. 要别人的东西:声明 needs + 往 calls 发请求

你不需要、也不许知道谁提供它。 两步:

① 配置里声明契约名(中立名,不是驱动名):

{
  "efi": 1, "name": "样例消费器", "runtime": "python", "mode": "resident",
  "entry": "请求.py", "interpreter": "system",
  "needs": ["样板:心跳"]
}

② 代码里发请求 → 等结论 → 取结果(完整可跑版见 驱动/样例消费器/请求.py):

# 发起方
游标.execute(
    "INSERT INTO calls (caller, want, args, lock_key)"
    " VALUES (%s, %s, %s::jsonb, %s) RETURNING id",
    ("样例消费器", "样板:心跳", json.dumps({"chain": ["样例消费器"]}, ensure_ascii=False), "样板:心跳"),
)
调用id = 游标.fetchone()[0]
# 之后轮询: SELECT state, provider, result, error FROM calls WHERE id = %s
# state 走到 done / failed / denied / timeout 就是有结论了 (等不到要如实说等不到, 别无限等)

被调的一方resident)自己 LISTEN driver_<我的名字>,内核用 pg_notify 叫醒它:

连接.execute("LISTEN driver_我的驱动名")        # 通道名只跟"我是谁"有关, 跟谁在调我无关
# 收到通知 -> payload 是 calls.id -> 干活 -> UPDATE calls SET state='done', result=… WHERE id=…

oneshot 的提供方不用自己 LISTEN:内核仲裁时发现它没在跑,会按需把它拉起来跑一遍。

排障、六条仲裁(越权/成环/无人提供/按需拉起/同锁排队/超时收权)、锁与超时的细节 → 04-契约与调用.md

6. 常驻驱动的三条纪律

  1. 收到 SIGTERM 要干净退出。装死不退,内核只能 SIGKILL(日志/状态都不好看):

    def 收工(: int, : object) -> None:
        print("我的驱动: 收到信号", , "收工", flush=True)
        sys.exit(0)
    signal.signal(signal.SIGTERM, 收工)
    signal.signal(signal.SIGINT, 收工)
    
  2. stdout 随便打,但要知道它去哪:你的 stdout/stderr 被内核重定向进 驱动/<名>/logs/<名>.log (每次启动前插一条 ==== 启动 … ==== 分隔头)。用 内核/内核.py 日志 <名> -f 实时跟。

  3. 打点节流:每 5s 打印一行是给人看的(老板 tail 得见它在活着),events 30s 一条就够。

7. 调试手法(按这个顺序查最快)

想确认 怎么做
文件夹被认了没 内核/内核.py 列表无效 的话看 说明 列 / 状态 <名> 的"上次错误"
内核怎么判它的 内核/内核.py 状态 问好 --json(含 PID / 命令行 / 配置是否待重启 / 重启次数 / 契约)
它说了什么 内核/内核.py 日志 <名> -n 50-f 实时跟)
内核怎么记账的 内核/内核.py 事件 -n 50内核/内核.py 日志 --内核
不起眼但最有用的一招 直接手动跑一遍python3 驱动/问好/问好.py(驱动是普通程序,不需要内核就能跑)
配置改完不生效 配置有 config_hash 指纹,改完要重启内核/内核.py 重启 <名>列表 会提示"待重启"

8. 驱动可以带 skill(归你管)

驱动不只会跑代码,还可以携带 skill —— 声明式文件放在 驱动/<名>/技能/*.json一个 skill 一个文件。 规矩只有一条:内核不知道 skill 的存在,读它、解释它、决定给哪个模型,全是驱动自己的事 (内核只管确定性代码能力的接入与仲裁,一旦它掺和 skill,"模型"这个概念就漏进内核了)。

同一件事写两份,分模型档(字段表与两档差别见 设计/05-技能规范.md):

给谁 写法
tier: "main" 云端强模型 / 本地 27B+ 步骤抽象 3 条,只锁外层结构,规划权交给模型
tier: "small" 本地 4B~9B 步骤写死编号 5 条,字段白名单 + 置信度只许三档,含反例 few-shot

最小骨架(完整样例看 驱动/样板技能/技能/请求解析-主模型.json):

{ "efi": 1, "name": "…", "version": "0.1", "tier": "main",
  "description": "一句话", "when": ["什么时候用"],
  "input": { "说明": "…", "schema": {} }, "steps": ["…"],
  "output": { "format": "json", "schema": {}, "说明": "…" },
  "examples": [ { "输入": {}, "输出": {} } ], "boundaries": ["不许编 / 不许猜"], "notes": ["给谁跑"] }

驱动怎么用:启动时读自己目录下的 技能/,按 tier 选模型、拼 prompt、校验输出、失败重试 —— 全在你手里驱动/样板技能/技能.py 是最小示范(只把自带的 skill 列出来打印,不执行)。 要真跑(按档选模型 → 拼 prompt → 调模型 → 剥 JSON → 逐条校验输出 → 落盘 + 写 events)看 驱动/技能执行/执行.py —— 它把同一份 small 档 skill 真喂给本地模型跑了一遍,两个反例(多步请求、信息不足) 都会按 skill 写的规矩回问而不是硬猜。

9. 交付一个驱动前的检查表

  • 文件夹根目录有 配置.efi.json,且 efi=1 / name 唯一 / runtime / entry 齐全
  • entry相对路径,不出驱动根(不许 ..
  • mode 选对(跑完就退的用 oneshot,长跑用 resident
  • autostart / restart 都是想要的(默认 false / no —— 手动控制口径)
  • 要别人的东西写的是契约名needs),代码里没出现别的驱动名
  • 只写 events / calls;碰不到 drivers / driver_state / commands
  • SIGTERM 干净退出(resident 必须)
  • 汇报失败、连不上 PG 都有兜底(别把驱动搞死、别装成功)
  • 内核/内核.py 扫描 → 启动 → 状态 → 日志 四步都看过一眼
  • 敏感信息走 env / 环境变量,没写进 配置.efi.json