最小 Agent 运行时内核:模型接入、工具循环、预算、可观测、持久化
Project description
ai-harness-framework
pip install ai-harness-framework
最小 Agent 运行时内核。把"调模型 → 调工具 → 再调模型"这个循环,连同它在真实场景里必然会撞上的那些失败模式,一起做成一个可复用的包。
不是 agent 框架,不替你写 prompt,不预设你的业务形状。它只负责一件事:让一个随机的模型,在一个确定性的壳子里可靠地干活。
消息 ──▶ ModelClient ──▶ tool_calls ──▶ ToolExecutor ──▶ 结果回填 ──┐
▲ │
└────────────── AgentLoop(预算 / 打转检测 / 快照)◀───────┘
为什么是这些模块
Agent 循环本身只有几十行,难的是它必须扛住的事。下面每一条都对应一次真实的翻车:
| 失败模式 | 对策 | 位置 |
|---|---|---|
| 模型返回的工具参数不符合 schema | 校验错误原样喂回,让模型自纠正,不崩 | tools/base.py |
| 工具调用参数不是合法 JSON | 同上,回填明确错误让它重发 | loop/agent_loop.py |
| 模型不肯停,一直调工具 | max_steps 硬上限 |
loop/agent_loop.py |
| 模型原地打转,反复同参调同一工具 | 签名窗口检测 → 注入纠偏 并同时升温 → 仍重复则中止 | loop/agent_loop.py |
| 模型换着花样撞同一堵墙(每次调用都不同,每次结果都一样) | 结果窗口检测 → 注入纠偏 → 仍不变则中止 | loop/agent_loop.py |
| token / 墙钟花超 | 步边界检查,超限即中止 | reliability/budget.py |
| 工具超时或抛异常 | 兜成 is_error 结果,不让单个工具搞崩整个 run |
tools/base.py |
| 端点 200 但空产出(内容安全拦截 / 截断) | 记 warning 留证,避免静默失败无从定位 | llm/openai_compat.py |
| 跑到一半崩了 | 步边界存快照,resume(run_id) 续跑 |
persistence/checkpoint.py |
危险命令(rm -rf 之类) |
分类拦截 → 请求人工确认 → 拒绝后强制模型如实交代 | shell/policy.py + approval.py |
关于"打转纠偏要连采样一起换":只改措辞不改温度时,模型很容易照着同一条采样路径再走一遍——"请换个思路"这句话本身也是低概率才被听进去的。这类细节是这个包存在的理由。
安装
需要 Python ≥ 3.11。核心只有 4 个依赖(openai / pydantic / pydantic-settings / opentelemetry-api),重依赖全部下放到 extras,按需装:
pip install ai-harness-framework # 内核:模型 + 工具 + 循环 + 预算 + 事件
pip install "ai-harness-framework[memory,mcp]" # 只要某几个特性
pip install "ai-harness-framework[all]" # 全部特性
用 uv 的话:
uv add ai-harness-framework
uv add "ai-harness-framework[all]"
| Extra | 装什么 | 用到的模块 |
|---|---|---|
telemetry |
opentelemetry-sdk + OTLP exporter | 真正导出 span(不装则退化为 no-op,内核照常运行) |
tokenizer |
tiktoken | 精确 token 计数(不装则走估算回退) |
sandbox-docker |
docker[ssh] | harness.sandbox.docker(LocalSandbox 不需要) |
http |
httpx | harness.tools.builtins.http_tool |
browser |
trafilatura | harness.browser 正文提取 |
mcp |
mcp | harness.mcp |
skills / orchestration |
pyyaml | 技能注册表 / 多 agent 名册 |
memory |
sqlite-vec、semantic-text-splitter、tree-sitter-* | harness.memory 向量检索 |
判定依据是模块级 import:惰性 import(函数体内 / try 块内)一律下放。只用内核的消费者不会被 docker、sqlite-vec 这类重依赖拖累。
快速上手
import asyncio
from pydantic import BaseModel
from harness.config import HarnessConfig
from harness.context.manager import ContextManager
from harness.events import RunError, RunFinished, TextDelta, ToolStarted
from harness.llm.openai_compat import OpenAICompatibleClient
from harness.loop.agent_loop import AgentLoop
from harness.reliability.budget import BudgetTracker
from harness.tools.base import Tool, ToolRegistry
class GetWeather(Tool):
name = "get_weather"
description = "查询某城市当前天气。"
class Params(BaseModel): # schema 从这里自动生成,也在这里自动校验
city: str
async def run(self, params: "GetWeather.Params") -> str:
return f"{params.city}:晴,24°C"
async def main() -> None:
config = HarnessConfig(
api_key="sk-...",
base_url="https://api.deepseek.com/v1", # 任意 OpenAI 兼容端点
model="deepseek-chat",
)
registry = ToolRegistry()
registry.register(GetWeather())
loop = AgentLoop(
client=OpenAICompatibleClient(config),
registry=registry,
context=ContextManager("你是一个乐于助人的助手。"),
model_name=config.model,
max_steps=10,
budget=BudgetTracker(max_tokens=100_000, max_wall_seconds=120),
loop_detect_window=3, # 连续 3 步同参调同一工具、或同一工具连续 3 次返回相同结果 → 判停滞
)
async for ev in loop.run("北京天气怎么样?"):
if isinstance(ev, TextDelta):
print(ev.text, end="", flush=True)
elif isinstance(ev, ToolStarted):
print(f"\n[调用 {ev.tool_call.name}]")
elif isinstance(ev, RunFinished):
print("\n[完成]")
elif isinstance(ev, RunError):
print(f"\n[出错] {ev.error}")
asyncio.run(main())
AgentLoop.run() 是一个异步事件流。事件类型见 harness/events.py——TextDelta / ReasoningDelta / ToolCallRequested / ToolStarted / ToolFinished / ModelUsage / ApprovalRequired / RunFinished / RunError 等。消费方决定怎么渲染,内核不假设你有没有 UI。
厂商适配
OpenAICompatibleClient 指向任意 OpenAI 兼容端点(DeepSeek、Kimi、智谱 GLM、通义 Qwen、vLLM、Ollama…),并处理了各家的实际差异:
- 思考模式参数不统一——上游只表达厂商中立的
enable_thinking意图,落成哪家的参数由适配层翻译(Qwen/百炼用enable_thinking,DeepSeek 用thinking={"type": ...})。 - 厂商识别按模型名优先、再看 base_url——统一网关下同一个
base_url转发多家模型时,靠 URL 判不出厂商。 - 不认某参数的模型直接剔除,避免端点因"未知参数"报错。
reasoning_content单独走ReasoningDelta事件,不混进正文。
模块地图
| 模块 | 职责 |
|---|---|
types / events / state |
核心数据类型与事件定义 |
llm/ |
ModelClient 协议、OpenAI 兼容实现、采样参数管理 |
tools/ |
Tool 基类、注册表、执行器(校验 + 兜底 + 截断) |
loop/ |
AgentLoop——步循环、打转检测、快照、span |
context/ |
每轮发什么消息:窗口、裁剪、预算 |
reliability/ |
预算追踪、重试退避 |
persistence/ |
checkpoint、序列化、轨迹落库 |
telemetry/ |
OpenTelemetry tracer |
approval.py |
人工审批闸门 |
sandbox/ |
执行隔离:Docker / Local |
shell/ net/ |
危险命令分类、SSRF 防护 |
skills/ orchestration/ mcp/ memory/ browser/ |
可选能力 |
分层契约
harness 是内核,不得反向依赖任何上层应用。这条边界由 tests/test_architecture_layering.py 用 AST 逐文件钉死——注释里提到 app 不算违规,只有真正的 import 语句才算。
随手写一句 from app.config import AppConfig 就能把内核焊死在某个产品上,而且不会有任何报错。所以它需要一个测试来守,而不是靠自觉。
消费者
- ai-learning-helper——学习助手。第一个消费者,本包即从其
src/harness抽出。 - ai-fix-code-loop——自动修 bug 的 agentic loop:测试失败进去,验证过的补丁出来。第二个消费者。
抽包时的验收标准就是:ai-learning-helper 的完整测试套件(1906 项)在 harness 来自本包而非其本地 src/ 的情况下全绿。一个只服务过一个应用的抽象,没人知道它是不是真的抽象。
开发
uv sync --all-extras --group dev
uv run pytest # 671 passed, 3 skipped
Python ≥ 3.11。
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ai_harness_framework-0.0.4.tar.gz.
File metadata
- Download URL: ai_harness_framework-0.0.4.tar.gz
- Upload date:
- Size: 298.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b59388fc5c9a7dd6c4dcbde7489494f55fc6099dbc9fe5e0cf806e0a253d5aa
|
|
| MD5 |
c14ae2f4a87afdc680177a4b0ed3fd27
|
|
| BLAKE2b-256 |
c8a8495f2a4c7a457994f6a8295e84983c54b9a39589c4fe7900777b4ac8bcac
|
Provenance
The following attestation bundles were made for ai_harness_framework-0.0.4.tar.gz:
Publisher:
release.yml on sumengnan/ai-harness-framework
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_harness_framework-0.0.4.tar.gz -
Subject digest:
2b59388fc5c9a7dd6c4dcbde7489494f55fc6099dbc9fe5e0cf806e0a253d5aa - Sigstore transparency entry: 2339245260
- Sigstore integration time:
-
Permalink:
sumengnan/ai-harness-framework@c66790e050c80f9cebb1b0d8fa362f95d72c31b2 -
Branch / Tag:
refs/tags/v0.0.4 - Owner: https://github.com/sumengnan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c66790e050c80f9cebb1b0d8fa362f95d72c31b2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ai_harness_framework-0.0.4-py3-none-any.whl.
File metadata
- Download URL: ai_harness_framework-0.0.4-py3-none-any.whl
- Upload date:
- Size: 141.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91e66d68049ac32ba1cf08e6969cfe1652a5771e20ef53f92995f90c6a95ea27
|
|
| MD5 |
f716c98d9a5b1f5f4edbe83d9879bd97
|
|
| BLAKE2b-256 |
11414df19e61fe432a8c3ad1fc9a99ab27335110a99f483ec14b1c3d6f69696b
|
Provenance
The following attestation bundles were made for ai_harness_framework-0.0.4-py3-none-any.whl:
Publisher:
release.yml on sumengnan/ai-harness-framework
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_harness_framework-0.0.4-py3-none-any.whl -
Subject digest:
91e66d68049ac32ba1cf08e6969cfe1652a5771e20ef53f92995f90c6a95ea27 - Sigstore transparency entry: 2339245273
- Sigstore integration time:
-
Permalink:
sumengnan/ai-harness-framework@c66790e050c80f9cebb1b0d8fa362f95d72c31b2 -
Branch / Tag:
refs/tags/v0.0.4 - Owner: https://github.com/sumengnan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c66790e050c80f9cebb1b0d8fa362f95d72c31b2 -
Trigger Event:
release
-
Statement type: