Files
efi-kernel/驱动/技能执行/执行.py
T
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

587 lines
26 KiB
Python

"""技能执行: 示例驱动 -- 把驱动自带的 skill 真正执行一遍.
[这个示例演示了什么]
1. 底座最下面那层是"确定性代码能力接入层". 驱动把一种外部能力接进来, 这里接的是
"一次模型调用"; 上面两层(引导器 / 内核)完全不知道下面在用模型.
2. **skill 归驱动管**. 读哪份 / 选哪个模型 / prompt 怎么拼 / 输出怎么校验 / 怎么记账,
全在本文件里, 内核不知道 skill 的存在 (依据 设计/01 第 7 节 与 设计/05 技能规范).
3. 两档 skill 的差别落到代码上 -- 同一件事写两份, 校验强度不同:
main : 只锁外层 (plan 是数组 + 每步有 want + confidence 在 0~1)
small: 全锁死 (want 单值 + 必须在契约清单里 + args 键白名单 + confidence 只许 0/0.5/1)
4. 与底座的全部接口只有两处:
环境变量 EFI_DB -- 内核注入的库连接串 (没有也能跑, 只是不汇报)
events 表 -- 汇报的唯一通道, 往表里插一行 (没有协议)
另外它**只读** drivers 表拿契约清单 -- 真实驱动就该这么拿底座里的信息, 而不是去
翻别的驱动文件夹 (驱动之间零耦合, 见 设计/01 第 3 节).
[怎么改]
模型端点 / 模型名 / 跑哪一档 / 喂什么话, 全在 配置.efi.json 的 env 段, 源码不硬编码;
密钥类(EFI_LLM_KEY)不写进配置, 从环境变量继承 (密钥不落盘).
不需要内核也能手工跑一遍: python3 驱动/技能执行/执行.py
"""
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime
from pathlib import Path
from typing import Any, cast
驱动名 = "技能执行"
驱动目录 = Path(__file__).resolve().parent
技能目录 = 驱动目录 / "技能"
输出目录 = 驱动目录 / "输出"
# 模型端点超时: 本地小模型带思考时会慢, 给宽一点; 但绝不无限等 (等不到就如实报错退出)
模型超时秒 = 240.0
# 单次最多让模型吐多少 token; 思考型模型会把预算全用在思考上, 所以 content 空时自动翻倍重试
模型默认最大输出 = 4096
最大输出封顶 = 16384
# 允许的置信度取值 (small 档写死的三档); 用集合判"是不是这三个之一"
小模型置信度 = {0.0, 0.5, 1.0}
# ---------------------------------------------------------------- 与底座打交道
def 取环境(键: str, 默认: str = "") -> str:
"""取一个配置值: 内核把 配置.efi.json 的 env 段注入环境变量, 手工跑就是普通环境变量."""
值 = os.environ.get(键, "").strip()
return 值 if 值 else 默认
def 汇报(消息: str, kind: str = "log", 数据: dict[str, Any] | None = None) -> None:
"""往 events 表插一行 -- 驱动唯一的汇报方式 (没有协议, 内核只读表, 不解析 stdout).
连不上 PG / 没装 psycopg2 都不算失败: 打印一行就够了 (设计 01 第 3 节). 汇报失败
也不许把驱动搞死. 这里刻意每次新开连接: 本驱动是一次性任务, 用完就关最省事.
"""
串 = os.environ.get("EFI_DB", "")
if not 串:
return
try:
import psycopg2 # type: ignore[import-untyped]
except ImportError:
return
try:
连接对象 = psycopg2.connect(串)
连接对象.autocommit = True
游标 = 连接对象.cursor()
游标.execute(
"INSERT INTO events (source, driver, level, kind, message, data)"
" VALUES (%s, %s, 'info', %s, %s, %s::jsonb)",
(驱动名, 驱动名, kind, 消息, json.dumps(数据 or {}, ensure_ascii=False)),
)
游标.close()
连接对象.close()
except Exception as 错: # 汇报失败不该把驱动搞死
print(f"{驱动名}: 汇报失败: {错}", flush=True)
def 读契约清单() -> list[dict[str, Any]]:
"""问底座"现在有哪些契约可以用" -- 只读 PG 的 drivers 表 (内核扫描时写进去的).
真实驱动就该这么拿信息: 不 import 别的驱动, 也不去翻别人的文件夹. 参数列表库里没有
(契约名本身不带参数说明), 一律给空表 -- 对应 skill 里那条"参数列表为空就表示这个契约
不接受参数, args 填 {}".
"""
显式 = 取环境("EFI_SKILL_CONTRACTS")
if 显式:
# 手工跑 / PG 不在时也能演示: 直接给一份清单
try:
读了: Any = json.loads(显式)
except json.JSONDecodeError as 错:
print(f"{驱动名}: EFI_SKILL_CONTRACTS 不是合法 JSON: {错}", flush=True)
return []
if isinstance(读了, list):
return [cast(dict[str, Any], 项) for 项 in cast(list[Any], 读了) if isinstance(项, dict)]
return []
串 = os.environ.get("EFI_DB", "")
if not 串:
print(f"{驱动名}: 没拿到 EFI_DB, 契约清单当空 (手工跑就这样, 不算错)", flush=True)
return []
try:
import psycopg2 # type: ignore[import-untyped]
连接对象 = psycopg2.connect(串)
连接对象.autocommit = True
游标 = 连接对象.cursor()
游标.execute("SELECT provides FROM drivers WHERE provides IS NOT NULL ORDER BY name")
行集 = 游标.fetchall()
游标.close()
连接对象.close()
except Exception as 错:
print(f"{驱动名}: 读 drivers 表失败 (当没有契约): {错}", flush=True)
return []
清单: list[dict[str, Any]] = []
for 行 in cast(list[Any], 行集):
契约们 = 行[0]
if not isinstance(契约们, list):
continue
for 契约 in cast(list[Any], 契约们):
文本 = str(契约).strip()
if 文本:
清单.append({"want": 文本, "参数": []})
return 清单
# ---------------------------------------------------------------- 解释 skill
def 取对象(值: Any) -> dict[str, Any]:
"""值确实是 dict 就给 dict, 否则空 dict -- 窄化只做一次, 别在代码里到处 cast."""
if isinstance(值, dict):
return cast(dict[str, Any], 值)
return {}
def 字符串表(值: Any) -> list[str]:
"""把 json 里读出来的 list 收成 list[str] (脏数据不炸: 非字符串项直接丢)."""
if not isinstance(值, list):
return []
出: list[str] = []
for 项 in cast(list[Any], 值):
if isinstance(项, str) and 项.strip():
出.append(项)
return 出
def 编号(项们: list[str]) -> str:
"""1. xxx / 2. xxx 排下来 (步骤和红线都用这个, 数字让模型逐步跟)."""
return "\n".join(f" {序}. {文}" for 序, 文 in enumerate(项们, 1))
def 读技能(档: str) -> tuple[str, dict[str, Any]] | None:
"""按 tier 挑一份 skill 读出来; 找不到 / 读不了就说清为什么 (不静默跳过).
skill 放 技能/*.json, 一个 skill 一个文件, 文件名不参与语义 (name 字段才是名字).
"""
if not 技能目录.is_dir():
print(f"{驱动名}: 没有 技能/ 目录, 没东西可执行", flush=True)
return None
有档: list[str] = []
for 路径 in sorted(技能目录.glob("*.json")):
try:
内容: Any = json.loads(路径.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as 错:
print(f"{驱动名}: 读不了 {路径.name}: {错}", flush=True)
continue
if not isinstance(内容, dict):
print(f"{驱动名}: {路径.name} 顶层必须是对象, 跳过", flush=True)
continue
对象 = cast(dict[str, Any], 内容)
现档 = str(对象.get("tier") or "")
有档.append(现档)
if 现档 == 档:
return 路径.name, 对象
print(f"{驱动名}: 没有 tier={档} 的 skill (现有档: {sorted(有档)})", flush=True)
return None
def 拼系统提示(技能: dict[str, Any]) -> str:
"""把 skill 声明拼成给模型的系统提示 -- 这就是"驱动解释 skill"的落点.
骨架只有一份: 两份 skill 的结构一样, 差别全在文件里写的步骤 / 输出结构 / 边界 / 样例,
所以**换档只是换一份文件, 换模型也不用改这里**.
"""
段: list[str] = []
段.append(f"你是 {驱动名} 里的解析器. 任务: {str(技能.get('description') or '')}")
触发 = 字符串表(技能.get("when"))
if 触发:
段.append("什么情况下用:\n" + 编号(触发))
步骤 = 字符串表(技能.get("steps"))
if 步骤:
段.append("执行步骤:\n" + 编号(步骤))
输出 = 取对象(技能.get("output"))
if 输出:
模式 = 取对象(输出.get("schema"))
if 模式:
字段行 = "\n".join(f" {键}: {值}" for 键, 值 in 模式.items())
段.append("输出结构 (必须是 JSON):\n" + 字段行)
说明 = str(输出.get("说明") or "")
if 说明:
段.append(说明)
样例 = 技能.get("examples")
if isinstance(样例, list) and cast(list[Any], 样例):
块: list[str] = []
for 项 in cast(list[Any], 样例):
对 = 取对象(项)
块.append("输入: " + json.dumps(对.get("输入"), ensure_ascii=False))
块.append("输出: " + json.dumps(对.get("输出"), ensure_ascii=False))
段.append("示例:\n" + "\n".join(块))
边界 = 字符串表(技能.get("boundaries"))
if 边界:
段.append("红线 (违反即错):\n" + 编号(边界))
段.append("只输出 JSON 本体: 前后不要任何解释文字, 不要 markdown 代码块标记.")
if 取环境("EFI_LLM_NO_THINK"):
# 带思考的模型(DeepSeek / Qwen3 系)会把 max_tokens 全用在思考上, content 回来是空串.
# Qwen3 系的软开关 /no_think 放在**提示末尾**才认, 比塞进请求体的字段更通用 (2026-09-17 实测).
段.append("/no_think")
return "\n\n".join(段)
def 拼用户输入(技能: dict[str, Any], 原话: str, 清单: list[dict[str, Any]]) -> str:
"""按这份 skill 自己声明的 input.schema 结构, 把真实输入拼成 JSON.
两档的输入字段名不一样 (main 要"可用契约"是字符串表, small 要"契约清单"带参数名),
这里**照 skill 写的 schema 填**, 不写死字段名 -- 又是一处"驱动解释 skill"而不是"驱动写死 skill".
"""
模式 = 取对象(取对象(技能.get("input")).get("schema"))
载荷: dict[str, Any] = {}
for 键 in 模式:
if 键 == "用户原话":
载荷[键] = 原话
elif 键 == "可用契约":
载荷[键] = [str(项.get("want") or "") for 项 in 清单]
elif 键 == "契约清单":
载荷[键] = 清单
else:
载荷[键] = None
if not 载荷: # skill 没写 input.schema 就退回一个最小结构, 不让模型空手答
载荷 = {"用户原话": 原话, "契约清单": 清单}
return json.dumps(载荷, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------- 调模型
def 建打开器(端点: str) -> urllib.request.OpenerDirector:
"""本机端点直连: 本机装了 Clash 这类代理时, http_proxy 会把 127.0.0.1 的请求也劫走."""
主机 = urllib.parse.urlsplit(端点).hostname or ""
if 主机 in ("127.0.0.1", "localhost", "::1"):
return urllib.request.build_opener(urllib.request.ProxyHandler({}))
return urllib.request.build_opener()
def 调模型(端点: str, 模型: str, 系统提示: str, 用户输入: str, 最大输出: int) -> tuple[bool, str, bool]:
"""调一次 OpenAI 兼容的 /chat/completions (只用标准库, 驱动不引第三方依赖).
返回 (成没成, 模型原文 或 错误原文, 是不是"思考把输出吃光了"). 失败**不吞错**,
也不编一个假结果出来; 最后那个标志位让调用方决定要不要加大预算重试一次.
"""
请求体: dict[str, Any] = {
"model": 模型,
"messages": [
{"role": "system", "content": 系统提示},
{"role": "user", "content": 用户输入},
],
"temperature": 0,
"max_tokens": 最大输出,
"stream": False,
}
if 取环境("EFI_LLM_NO_THINK"):
# 带思考的模型(DeepSeek / Qwen3 系)会把 max_tokens 全用在思考上, content 回来是空串.
# 这是 Qwen3 系的通用写法; 换成不认这个字段的端点(如 OpenAI 官方)就把配置里那行删掉.
请求体["chat_template_kwargs"] = {"enable_thinking": False}
头 = {"Content-Type": "application/json"}
密钥 = 取环境("EFI_LLM_KEY")
if 密钥:
头["Authorization"] = f"Bearer {密钥}"
请求 = urllib.request.Request(
url=端点.rstrip("/") + "/chat/completions",
data=json.dumps(请求体, ensure_ascii=False).encode("utf-8"),
headers=头,
method="POST",
)
打开器 = 建打开器(端点)
try:
with 打开器.open(请求, timeout=模型超时秒) as 应答:
原文 = 应答.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as 错:
try:
详情 = 错.read().decode("utf-8", errors="replace")
except OSError:
详情 = ""
return False, f"模型端点回了 HTTP {错.code}: {详情[:400]}", False
except (urllib.error.URLError, TimeoutError, OSError) as 错:
return False, f"连不上模型端点 {端点}: {错}", False
try:
包: Any = json.loads(原文)
except json.JSONDecodeError as 错:
return False, f"端点回的不是 JSON: {错} / 原文 {原文[:200]}", False
选择 = 取对象(包).get("choices")
if not isinstance(选择, list) or not cast(list[Any], 选择):
return False, f"端点没给 choices: {原文[:300]}", False
首项 = 取对象(cast(list[Any], 选择)[0])
内容 = 取对象(首项.get("message")).get("content")
if not isinstance(内容, str) or not 内容.strip():
# 带思考的模型把 max_tokens 全用在思考上时, content 就是空的 (2026-09-17 真踩到)
return False, (
"模型 content 是空的 (带思考的模型把 max_tokens 用在思考上了; "
"配置里开 EFI_LLM_NO_THINK=1 关掉思考, 或者把 max_tokens / 上下文调大): "
+ 原文[:300]
), True
return True, 内容, False
def 剥JSON(文本: str) -> tuple[bool, Any]:
"""从模型原文里抠出 JSON 对象 -- 模型爱加 ``` 围栏或前后废话, 清洗是驱动该干的活."""
清洗 = 文本.strip()
if 清洗.startswith("```"):
换行 = 清洗.find("\n")
清洗 = 清洗[换行 + 1 :] if 换行 >= 0 else 清洗
右围栏 = 清洗.rfind("```")
if 右围栏 >= 0:
清洗 = 清洗[:右围栏]
清洗 = 清洗.strip()
try:
return True, json.loads(清洗)
except json.JSONDecodeError:
pass
# 兜底: 取第一个 { 到最后一个 } (前后还有废话时用)
左 = 清洗.find("{")
右 = 清洗.rfind("}")
if 左 >= 0 and 右 > 左:
try:
return True, json.loads(清洗[左 : 右 + 1])
except json.JSONDecodeError as 错:
return False, f"剥出来的片段不是合法 JSON: {错}"
return False, "模型输出里没有 JSON 对象"
# ---------------------------------------------------------------- 校验输出
def 校验输出(档: str, 结果: Any, 清单: list[dict[str, Any]]) -> list[tuple[bool, str]]:
"""按这一档的规矩逐条核对模型输出 (每条给"判据 + 结论", 可解释、可审计).
main : 只锁外层 -- plan 是数组 + 每步有 want + confidence 在 0~1
small: 全锁死 -- want 单值 + 必须在契约清单里 + args 键白名单 + confidence 只许 0/0.5/1
"""
判据: list[tuple[bool, str]] = []
对象 = 取对象(结果)
if not 对象:
判据.append((False, "顶层是 JSON 对象"))
return 判据
判据.append((True, "顶层是 JSON 对象"))
if 档 == "small":
判据.extend(_校小模型(对象, 清单))
else:
判据.extend(_校主模型(对象))
return 判据
def _校小模型(对象: dict[str, Any], 清单: list[dict[str, Any]]) -> list[tuple[bool, str]]:
"""small 档的校验: 结构全锁死, 差一格都算不过 (这一档靠结构稳, 不靠模型聪明)."""
判据: list[tuple[bool, str]] = []
可用 = {str(项.get("want") or ""): 字符串表(项.get("参数")) for 项 in 清单}
if "want" not in 对象:
判据.append((False, "有 want 字段"))
return 判据
要的 = 对象.get("want")
if 要的 is None:
判据.append((True, "want 是 null (回问, 合法)"))
elif isinstance(要的, str):
判据.append((True, "want 是字符串"))
if 要的 in 可用:
判据.append((True, f"want 在契约清单里 ({要的})"))
elif not 可用:
判据.append((False, f"want={要的} 但契约清单是空的 (不许自造契约名)"))
else:
判据.append((False, f"want={要的} 不在契约清单里 ({sorted(可用)})"))
else:
判据.append((False, "want 必须是字符串或 null"))
参数 = 对象.get("args")
if isinstance(参数, dict):
参数对象 = cast(dict[str, Any], 参数)
判据.append((True, "args 是对象"))
白名单 = 可用.get(str(要的 or ""), [])
越界 = [键 for 键 in 参数对象 if 键 not in 白名单]
if 越界:
判据.append((False, f"args 里有多余的键 {越界} (白名单 {白名单})"))
else:
判据.append((True, f"args 的键都在白名单里 {白名单}"))
else:
判据.append((False, "args 必须是对象"))
置信 = 对象.get("confidence")
if isinstance(置信, bool) or not isinstance(置信, (int, float)):
判据.append((False, "confidence 必须是 0 / 0.5 / 1 之一"))
elif float(置信) in 小模型置信度:
判据.append((True, f"confidence={float(置信)} 在三档之内"))
else:
判据.append((False, f"confidence={置信} 不在 {{0, 0.5, 1}} 里"))
if 要的 is None:
回问 = 对象.get("need_user")
if isinstance(回问, str) and 回问.strip():
判据.append((True, "want 为 null 时给了 need_user"))
else:
判据.append((False, "want 为 null 却没给 need_user"))
return 判据
def _校主模型(对象: dict[str, Any]) -> list[tuple[bool, str]]:
"""main 档的校验: 只锁外层结构 (plan 是数组、每步有 want),细节交给模型."""
判据: list[tuple[bool, str]] = []
计划 = 对象.get("plan")
if not isinstance(计划, list):
判据.append((False, "plan 是数组"))
return 判据
步们 = cast(list[Any], 计划)
判据.append((True, f"plan 是数组 ({len(步们)} 步)"))
坏步: list[int] = []
for 序, 步 in enumerate(步们, 1):
步对象 = 取对象(步)
要的 = 步对象.get("want")
if not isinstance(要的, str) or not 要的.strip():
坏步.append(序)
if 坏步:
判据.append((False, f"第 {坏步} 步缺 want (每步都必须有)"))
else:
判据.append((True, "每一步都有 want"))
# plan 空 + 也不回问 = 既没规划也没说缺什么, 等于什么都没答 (2026-09-17 实测抓到)
回问 = 对象.get("need_user")
有话说 = isinstance(回问, str) and bool(回问.strip())
if 步们 or 有话说:
判据.append((True, "plan 非空, 或者给了 need_user (有结论)"))
else:
判据.append((False, "plan 是空的, 又没有 need_user (等于没答)"))
置信 = 对象.get("confidence")
if isinstance(置信, bool) or not isinstance(置信, (int, float)):
判据.append((False, "confidence 必须是 0~1 的数"))
elif 0.0 <= float(置信) <= 1.0:
判据.append((True, f"confidence={float(置信)} 在 0~1"))
else:
判据.append((False, f"confidence={置信} 超出 0~1"))
return 判据
# ---------------------------------------------------------------- 主流程
def 现在文本() -> str:
"""带时区的时刻 (项目里一律带 +08:00, 事后看日志不用猜时区)."""
return datetime.now().astimezone().isoformat(timespec="seconds")
def 落盘(记录: dict[str, Any]) -> Path:
"""把这一趟的原样记下来 (skill / 输入 / 提示 / 模型原文 / 校验结论), 方便事后审计."""
输出目录.mkdir(parents=True, exist_ok=True)
路径 = 输出目录 / f"结果-{datetime.now().strftime('%Y%m%d-%H%M%S')}.json"
路径.write_text(json.dumps(记录, ensure_ascii=False, indent=2), encoding="utf-8")
return 路径
def 主() -> int:
"""跑一趟: 读 skill -> 拿契约清单 -> 拼 prompt -> 调模型 -> 剥 JSON -> 校验 -> 记账."""
档 = 取环境("EFI_SKILL_TIER", "small")
原话 = 取环境("EFI_SKILL_INPUT")
端点 = 取环境("EFI_LLM_BASE")
模型 = 取环境("EFI_LLM_MODEL_MAIN" if 档 == "main" else "EFI_LLM_MODEL_SMALL")
print(f"{驱动名}: 示例驱动 (skill 归驱动管, 内核不知道它的存在)", flush=True)
读到的 = 读技能(档)
if 读到的 is None:
汇报(f"{驱动名} 执行失败: 没有 tier={档} 的 skill", kind="error")
return 1
文件名, 技能 = 读到的
步数 = len(字符串表(技能.get("steps")))
样例数 = len(技能.get("examples") or [])
边界数 = len(字符串表(技能.get("boundaries")))
print(f"{驱动名}: skill [{档}] {技能.get('name') or 文件名} (步骤 {步数} / 样例 {样例数} / 边界 {边界数})", flush=True)
if not 原话:
print(f"{驱动名}: 没给 EFI_SKILL_INPUT, 不知道要解析什么", flush=True)
汇报(f"{驱动名} 执行失败: 没给输入", kind="error")
return 1
if not 端点:
print(f"{驱动名}: 没给 EFI_LLM_BASE (模型端点), 没法执行 -- 看 配置.efi.json 的 env 段", flush=True)
汇报(f"{驱动名} 执行失败: 没配模型端点", kind="error")
return 1
清单 = 读契约清单()
来源 = "环境变量" if 取环境("EFI_SKILL_CONTRACTS") else "PG 的 drivers 表"
print(f"{驱动名}: 契约清单 {len(清单)} 条 (来源 {来源}): {[项['want'] for 项 in 清单]}", flush=True)
系统提示 = 拼系统提示(技能)
用户输入 = 拼用户输入(技能, 原话, 清单)
print(f"{驱动名}: 模型 {模型} @ {端点} 输入: {原话}", flush=True)
print(f"{驱动名}: 提示拼好 {len(系统提示)} 字, 调模型中 (超时 {int(模型超时秒)} 秒)...", flush=True)
起 = time.monotonic()
# max_tokens 可以配置; 没配就用默认. 思考型模型会把预算全用在思考上 -> content 空,
# 那就**加大预算重试一次**(设计 05 把"重试"明确划给驱动实现), 还空就如实报错退出.
预算文本 = 取环境("EFI_LLM_MAX_TOKENS")
最大输出 = int(预算文本) if 预算文本.isdigit() and int(预算文本) > 0 else 模型默认最大输出
成, 原文, 被吃光 = 调模型(端点, 模型, 系统提示, 用户输入, 最大输出)
while (not 成) and 被吃光 and 最大输出 < 最大输出封顶:
最大输出 = min(最大输出 * 2, 最大输出封顶)
print(f"{驱动名}: 思考把输出预算吃光了, max_tokens 提到 {最大输出} 重试一次...", flush=True)
成, 原文, 被吃光 = 调模型(端点, 模型, 系统提示, 用户输入, 最大输出)
用时 = time.monotonic() - 起
if not 成:
print(f"{驱动名}: 调模型失败 ({用时:.1f}s): {原文}", flush=True)
汇报(f"{驱动名} 调模型失败: {原文}", kind="error", 数据={"模型": 模型, "端点": 端点})
return 1
print(f"{驱动名}: 模型回了 {len(原文)} 字, 用时 {用时:.1f}s", flush=True)
剥好, 结果 = 剥JSON(原文)
判据: list[tuple[bool, str]] = []
if not 剥好:
判据.append((False, str(结果)))
else:
print(f"{驱动名}: 模型输出 = {json.dumps(结果, ensure_ascii=False)}", flush=True)
判据 = 校验输出(档, 结果, 清单)
print(f"{驱动名}: 校验 --", flush=True)
for 过没过, 说明 in 判据:
print(f" [{'通过' if 过没过 else '不过'}] {说明}", flush=True)
通过数 = sum(1 for 过没过, _ in 判据 if 过没过)
记录: dict[str, Any] = {
"技能": str(技能.get("name") or 文件名),
"档": 档,
"模型": 模型,
"端点": 端点,
"输入": 原话,
"契约清单": 清单,
"系统提示": 系统提示,
"用户输入": 用户输入,
"模型原文": 原文,
"模型输出": 结果 if 剥好 else None,
"校验": [{"判据": 说明, "通过": 过没过} for 过没过, 说明 in 判据],
"用时秒": round(用时, 1),
"时刻": 现在文本(),
}
路径 = 落盘(记录)
print(f"{驱动名}: 原样记到 {路径}", flush=True)
全过 = bool(判据) and 通过数 == len(判据)
if 全过:
汇报(
f"{驱动名}: {档} 档执行成功 ({技能.get('name') or 文件名}, {通过数}/{len(判据)} 判据通过)",
kind="produce",
数据={"模型": 模型, "档": 档, "结果": 结果, "用时秒": round(用时, 1)},
)
print(f"{驱动名}: 收工 -- {通过数}/{len(判据)} 条判据全过", flush=True)
return 0
汇报(
f"{驱动名}: {档} 档输出没过校验 ({通过数}/{len(判据)})",
kind="error",
数据={"模型": 模型, "档": 档, "结果": 结果, "用时秒": round(用时, 1)},
)
print(f"{驱动名}: 收工 -- 只过了 {通过数}/{len(判据)} 条, 退出码 1 (不装成功)", flush=True)
return 1
if __name__ == "__main__":
sys.exit(主())