AnilKerai/model-harness
GitHub: AnilKerai/model-harness
一个 .NET Agent 框架,通过可替换的端口化架构将原始模型转化为可控、可扩展的生产级 Agent。
Stars: 0 | Forks: 0
# Model Harness
**一个适用于 .NET 8 和 .NET 10 的端口与适配器(ports-and-adapters)Agent 框架。** Agent 的本质是 *模型 + 框架(harness)* —— model-harness 就是这个框架:它是将原始模型转化为可控 Agent 的循环、Guide、Sensor 和 Budget 的集合。每一个动态部分都是一个带有可用默认实现的 Port,因此你可以用几行代码配置出标准的 Agent,也可以在无需改动循环的情况下替换其中的任何单个组件。
[](https://www.nuget.org/packages/SapphireGuard.ModelHarness)
[](https://github.com/AnilKerai/model-harness/actions/workflows/ci.yml)
[](LICENSE)

*我写这个项目是为了通过构建一个系统来学习 Agentic 系统的工作原理,并用它来教导我的团队。
真心欢迎指正与批评 —— [详情见下](#where-this-came-from)。*
## 为什么选择 model-harness
**花更少的资源,做更多的事。** 普遍的假设是,更好的 Agent 需要更大的前沿模型。这个框架测试了相反的假设:一个结构良好的框架(harness)可以弥合很大一部分差距 —— 足以让更小、更便宜或本地托管的模型在运行相同任务时达到*可接受*的质量。实际的目标是在 7B 本地模型上将 `ClaudeModelClient` 替换为 `OllamaModelClient`,并且仍然能以极低的成本获得可用的结果。这个标准的制定是产品层面的决策,而不是模型层面的决策。
该框架通过将每一个控制点变成一个带有合理默认值的可替换 Port 来实现这一点:
- **一切皆为扩展点。** 上下文塑造、安全检查、Budget、rate limiting、Memory、compaction、tracing、checkpointing 和模型传输等,每一个都是你可以通过 builder 替换的命名 Port —— 无需更改循环。 [→ 扩展点](#extension-points)
- **两种模式,全面掌控。** [Guide](#the-guide-pattern--shaping-perception) 塑造模型每一轮看到的内容;[Sensor](#the-sensor-pattern--observing-and-intervening) 在五个 hookpoint 处进行观察和干预。大多数功能完全是通过这两种模式构建的。
- **构建即受限。** Turn、token、cost 和 wall-clock 都是[硬性限制](#budget-enforcement),每一轮都会进行检查;耗尽时会返回部分结果,而不会抛出异常。
- **开箱即用。** Prompt 注入防御、PII 脱敏、循环/卡死检测、Taint tracking、技能/学习、增量 compaction、sub-agents 和 human-in-the-loop 皆随框架内置。[→ 包含的功能](#batteries-included)
- **接入任意模型。** 目前已提供 Anthropic、Azure OpenAI / AI Foundry 和 Ollama 的适配器;`IModelClient` 是接入任何其他模型的 Port。
- **面向生产环境。** OpenTelemetry GenAI spans 和 metrics、checkpoint/resume、circuit-breaker 韧性机制和 prompt caching 均已配置好,或者只需一次调用即可实现。
## 快速开始
`AddStandardModelHarness` 是推荐的入口点 —— 提供一个模型、你的工具以及任何覆盖配置:
```
var services = new ServiceCollection();
services.AddStandardModelHarness(builder => builder
.WithSystemPrompt("You are a helpful assistant.")
.WithConsoleTracer()
.WithTool()
.WithClaudeModel(new ClaudeClientOptions { ApiKey = apiKey }));
await using var provider = services.BuildServiceProvider();
var outcome = await provider.GetRequiredService()
.RunAsync("What is 6 times 7?");
Console.WriteLine(outcome.FinalAnswer);
```
没有 API Key?样例会回退到 `FakeModelClient`,因此无需 Key 也能运行框架。在 [`getting-started/`](getting-started/) 中有一个最小可运行的项目 —— 打开 `GettingStarted.slnx`,放入一个包含你 Key 的 `appsettings.local.json`,然后运行。接着:**[RUNNING.md](docs/RUNNING.md)** 介绍如何运行样例,**[EXTENDING.md](docs/EXTENDING.md)** 提供实操指南,**[PRIMER.md](docs/PRIMER.md)** 讲解其背后的理念。
## 扩展点
该框架的每一个部分都是你可以通过 builder 替换的 Port —— 但需要清楚其实际附带的内容:许多 Port 默认是刻意的 **no-op**,一些具有开箱即用的**真实**默认值,还有几个需要你**自己提供**。下面是全景图及这三组分类:
```
flowchart LR
MC["IModelClient\nmodel transport"] --- LOOP
BE["IBudgetEnforcer\nbudget policy"] --- LOOP
RL["IRateLimiter\nrate policy"] --- LOOP
CP["ICheckpointStore\ncheckpoint / resume"] --- LOOP
TR["ITracer\ntracing & metrics"] --- LOOP
LOOP(["HarnessLoop"])
LOOP --- GP["IGuide\ncontext shaping"]
LOOP --- SN["ISensor\nobservation & intervention"]
LOOP --- TL["ITool / IToolRegistry\ntool dispatch"]
GP --- MS["IMemoryStore\nmemory retrieval"]
GP --- TS["IToolSelector\ntool filtering"]
GP --- SS["ISkillStore\nskills & learning"]
GP --- TG["ITrajectoryGuide\ntrajectory rendering"]
TG --- CS["ICompactionStrategy\ncompaction"]
TL --- HN["IHumanNotifier\nhuman-in-the-loop"]
```
### 开箱即用 —— 真实默认值
除非你进行覆盖,否则将运行真实的实现。
| Port | 默认实现 | 功能 |
|---|---|---|
| `IBudgetEnforcer` | `DefaultBudgetEnforcer` | 在每轮开始时强制执行 turn / token / cost / wall-clock 限制;在资源耗尽时返回 `PartialResult` 而不是抛出异常。 |
| `IContextBuilder` | `DefaultContextBuilder` | 将 Guide 生成的 `ContextDraft` 组装成最终的 prompt。 |
| `ITrajectoryGuide` | `HeadEvictionTrajectoryGuide` | 将历史记录渲染到上下文窗口中,在预算紧张时剔除最早的步骤。总是最后运行。 |
| `IToolSelector` | `PassthroughToolSelector` | 过滤模型每轮看到的工具 —— 默认为所有工具。 |
| `IToolRegistry` | `InMemoryToolRegistry` *(标准)* | 持有并分发工具。基础的 `AddModelHarness` 会以空的 `NullToolRegistry` 启动。 |
| `ITracer` | `OpenTelemetryTracer` *(标准)* | 嵌套的 `gen_ai.*` spans + metrics;针对 Sensor、Guide、compaction、checkpoint、rate-limit 等待和预算消耗的每轮事件。基础配置使用 `NullTracer`;添加 `WithConsoleTracer()` / `WithOtelTracer()`。 |
### 除非主动启用,否则为 no-op
Null 默认值在配置真实实现之前不起作用 —— 不使用即零成本。
| Port | 默认实现 | 启用方式 |
|---|---|---|
| `IMemoryStore` | `NullMemoryStore` | 用于检索增强上下文的向量数据库或知识图谱,由最新的用户回合查询。 |
| `ISkillStore` | `NullSkillStore` | `WithSkills` / `WithLearning` —— Agent 读取和写入的 `SKILL.md` 流程。 |
| `ICompactionStrategy` | `NullCompactionStrategy` | `WithAiCompaction(...)` —— 合并滚动摘要,而不是基础的省略提示。 |
| `ICheckpointStore` | `NullCheckpointStore` | `FileCheckpointStore` —— 每轮保存 `AgentState`,并在崩溃后恢复。 |
| `IRateLimiter` | `NullRateLimiter` | 在每次调用模型前检查的提供商 sliding-window 限流器。 |
| `IHumanNotifier` | `NullHumanNotifier` | 用于传递 `ask_human` 问题并以 `AwaitingHuman` 状态挂起运行的通道。 |
### 需由你提供
没有默认值 —— 框架需要你提供这些。最后三个是**累加式**的:你注册的内容会与内置组件一起运行。
| Port | 默认实现 | 描述 |
|---|---|---|
| `IModelClient` | 无 —— **必填** | 模型传输。通过提供商的便捷方法(`.WithClaudeModel(...)` / `.WithOllamaModel(...)` / `.WithAzureOpenAIModel(...)`)、通用的 `.WithModel(...)` 或用于生产级 circuit breaker 的 `.WithResilientModel(...)` 提供。 |
| `ITool` | 无 | 领域工具 —— Agent 影响外部世界的唯一方式。 |
| `ISensor` | `StuckDetector`, `ProgressCheckSensor`, `PromptInjectionSensor` *(标准)* | 在五个 hookpoint 观察和干预;基础配置不注册任何 Sensor。 |
| `IGuide` | 七个内置 Guide | 塑造模型每轮看到的内容;自定义 Guide 在 trajectory guide 之前插入。 |
除了 Port 之外:工具可以将参考内容(`ToolResult.Pins`)固定在不可剔除的区域中,从而在 compaction 后依然保留;任何 `ITool` 都可以包装一个嵌套的 `HarnessLoop` 作为完全隔离的 sub-agent —— 参见 [EXTENDING.md](docs/EXTENDING.md)。
## 核心模式
该框架建立在两种可组合的模式之上,它们共同实现了
在不修改循环的情况下对 Agent 行为进行细粒度控制。其背后的 Agentic-AI 理论写在 [PRIMER.md](docs/PRIMER.md) 中。
### Guide 模式 —— 塑造感知
**Guide** 控制模型每一轮看到的内容。在每次调用模型之前,
所有已注册的 Guide 按顺序运行,每个都为共享的 `ContextDraft` 做出贡献。
`DefaultContextBuilder` 随后将 draft 组装成最终的 prompt。
```
flowchart LR
A([All registered tools]) --> D
subgraph "Guide pipeline — runs before every model call"
direction LR
D[ContextDraft\ninitialised] --> G1[HarnessInstructionsGuide\nappends harness conventions]
G1 --> GR[ReActGuide\nprimes reason + act]
GR --> G2[MemoryGuide\nsurfaces snippets]
G2 --> G3[ToolSelectorGuide\nfilters AvailableTools]
G3 --> G4[ToolCatalogueGuide\nrenders tool catalogue]
G4 --> G5[SkillsGuide\nrenders skill catalogue]
G5 --> GN[... custom guides]
GN --> G6[TrajectoryGuide\nrenders history — always last]
end
G6 --> CB[DefaultContextBuilder\nassembles prompt]
CB --> M([Model call])
```
每个 Guide 都会接收完整的 `ContextDraft` 和当前的 `AgentState`,并
写入 draft 的一个或多个字段中:
| 字段 | 用途 |
|---|---|
| `SystemPrompt` | Agent 身份和常驻指令 |
| `TrajectoryMessages` | 渲染的历史 —— 模型 turn、工具结果、Sensor 备注 |
| `MemorySnippets` | 从检索系统中提取的长期知识,由最新的用户 turn 进行查询 |
| `AvailableTools` | 当前 turn 的工具列表 —— Guide 可以进行过滤或重排序 |
| `SystemSections` | 预渲染的 system prompt 部分(工具目录、技能目录),追加在 prompt 之后 |
每一个字段都是关于模型在此 turn 看到什么内容的显式选择 —— `ContextDraft` 是框架对上下文工程决策的具体表现。实现 `IGuide` 即可在不触碰循环的情况下更改这些选择。
有关 `IGuide` 接口和注册方式,请参见 [EXTENDING.md](docs/EXTENDING.md)。流水线的顺序是明确且固定的。有两个顺序约束驱动了这一点:
- **`ToolSelectorGuide` 在 `ToolCatalogueGuide` 之前** —— 目录会渲染 selector 为当前 turn 批准的所有工具;反转它们将始终渲染完整的工具列表,从而使过滤失效。
- **`HeadEvictionTrajectoryGuide` 必须最后运行** —— 它会计算已经写入 `ContextDraft`(`SystemPrompt`、`MemorySnippets`、`SystemSections`)的所有内容的 token 成本,从而计算出 trajectory 剩余的上下文窗口。如果先运行它,则意味着在固定的预留量下猜测该成本。这个约束是通过结构化方式强制执行的:`HeadEvictionTrajectoryGuide` 实现了 `ITrajectoryGuide`(而不是 `IGuide`),`DefaultGuideRunner` 将其作为独立的依赖项解析,并始终在所有 `IGuide` 实例之后调用它 —— 不依赖 DI 注册顺序。可以通过 `builder.WithTrajectoryGuide()` 替换默认实现。
通过 `builder.WithGuide()` 注册的自定义 Guide 会插入到内置组件之后、`HeadEvictionTrajectoryGuide` 之前。
像 `HarnessInstructionsGuide` 和 `ReActGuide` 追加到的常驻 system prompt 本身是由 `SystemPromptGuide` 设置的,当你调用 `builder.WithSystemPrompt(...)` 时会添加它。它是通过该 builder 调用注册的,而不是通过默认流水线,因此它不属于常驻内置 Guide 之一。由于每个 Guide 都贡献给同一个共享的 `SystemPrompt`,因此追加 prompt 的 Guide 之间的顺序无关紧要。
`ReActGuide` 实现了 [ReAct](https://arxiv.org/abs/2210.03629) 模式:它引导模型在每一轮中将推理(一行 *Thought*)与操作(工具调用)以及对每个结果的 *Observations* 交替进行。操作/观察部分已经是循环的一部分 —— 模型发出工具调用,框架分发它们并反馈结果 —— 因此这个 Guide 只是 system prompt 层面的引导,旨在让推理过程变得显式且可检查。
作为补充,`HeadEvictionTrajectoryGuide` 会在每一轮将原始任务文本作为 `[ORIGINAL GOAL]` 系统备注重新注入,这样即使 trajectory compaction 丢弃了早期历史,模型也不会偏离其初始意图。
### Sensor 模式 —— 观察与干预
**Sensor** 在声明的 hookpoint 处观察循环,并可以通过返回 `SensorResult.Intervene(reason)` 来提出关切。循环对该关切的响应取决于 hookpoint —— Sensor 不直接控制流程。Sensor 在每个 hookpoint 处**并行**运行 —— 它们独立观察且不共享状态。
```
flowchart TD
L([Loop reaches a HookPoint]) --> SR[ISensorRunner\nfans out in parallel]
SR --> S1[Sensor A]
SR --> S2[Sensor B]
SR --> SN[Sensor N ...]
S1 -- Pass --> M{Any intervene?}
S2 -- Pass --> M
SN -- Intervene: reason --> M
M -- No --> CONT([Continue normally])
M -- Yes --> INT[SensorInterventionStep\nappended to trajectory]
INT --> NEXT([Next guide pass renders\nit as an assistant message])
```
五个 hookpoint 及其典型用途,以及当 Sensor 干预时循环的响应:
| HookPoint | 触发时机 | 典型用途 | 干预时执行的操作 |
|---|---|---|---|
| `PreModelCall` | 在构建上下文和调用模型之前 | 目标偏移警告、连续错误警报、条件性预推理引导 | **标注** —— 该备注被追加到 trajectory 中,模型调用在同一 turn 继续进行,以便模型能立即采取行动。Rate limiting 属于 `IRateLimiter`;硬性 cost 限制属于 `IBudgetEnforcer。这两者都不属于这里。 |
| `PostModelCall` | 在模型响应之后,在执行操作之前 | PII 检测、输出过滤 | **拒绝** —— 响应被从 trajectory 中抑制,因此模型无法重新看到被标记的内容;模型获得一个新的 turn 来产生干净的响应。 |
| `PreToolCall` | 在分发每个工具之前 | 策略执行、授权 | **阻止** —— 工具永远不会被分发;会记录一条 `IsError = true` 的 `ToolCallStep`,以便模型看到干净的错误并可以重新规划。 |
| `PostToolCall` | 在收到每个工具结果之后 | 结果验证、审计日志 | **标记** —— 仅供参考;工具已经运行,其结果已在 trajectory 中。干预作为 assistant 消息被记录;模型仍然可以基于结果进行推理。如果你需要阻止执行,请使用 `PreToolCall`。 |
| `PreReturn` | 在向调用者返回最终答案之前 | 答案质量检查 | **质疑** —— 答案不被接受;模型获得一个新的 turn,并且可以看到它之前的响应,以便它看到自己说了什么并自我纠正。 |
Sensor 可以阻止操作,但绝不能夺走模型的 turn —— 模型总是会获得下一次调用,以便它可以自我纠正。每个 hookpoint 都有一个精确的动词:
annotate(`PreModelCall`)、reject(`PostModelCall`)、block(`PreToolCall`)、flag(`PostToolCall`)、challenge(`PreReturn`)。干预会将 Sensor 的 reason 包装在 `SensorInterventionStep` 中并追加到 trajectory。在下一 turn(对于 `PreModelCall` 是同一 turn),`HeadEvictionTrajectoryGuide` 会将其渲染为前缀为 `[HARNESS OBSERVATION — ...]` 的 assistant 角色消息。`HarnessInstructionsGuide` 会在最开始(在 system prompt 中)告诉模型这些备注的含义,以及必须将它们视为指令 ——
这是 Sensor 反馈的前馈补充。干预记录与工具调用历史是分开的,因此工具历史保持干净。
有关 `ISensor` 接口和注册方式,请参见 [EXTENDING.md](docs/EXTENDING.md)。
### Guide 和 Sensor 如何协同工作
Sensor 进行干预;Guide 决定模型从该干预中学到什么。
循环本身并不了解这两种模式的语义 —— 它只是运行 runner 并记录步骤。
```
Sensor intervenes at PreToolCall
│
▼
SensorInterventionStep appended to AgentState.Trajectory
│
▼ (next turn)
TrajectoryGuide renders it as an assistant-role message in ContextDraft
│
▼
Model sees: "[HARNESS OBSERVATION — my-sensor at PreToolCall] My previous response was blocked: dangerous-tool is not permitted. I will comply fully and not repeat this behaviour."
│
▼
Model re-plans without that tool
```
## 循环 (`HarnessLoop`)
```
sequenceDiagram
participant C as Caller
participant L as HarnessLoop
participant CP as ICheckpointStore
participant B as IBudgetEnforcer
participant RL as IRateLimiter
participant S as ISensorRunner
participant CB as IContextBuilder
participant M as IModelClient
participant R as IToolRegistry
C->>L: RunAsync(AgentState)
loop Each turn
L->>CP: SaveAsync(Checkpoint{turn, state})
L->>B: Check(state, startedAt)
alt Budget exhausted
B-->>L: Exhausted(reason)
L->>CB: BuildAsync — force-finalise prompt
L->>M: CallAsync (no tools)
M-->>L: ModelResponse
L-->>C: AgentOutcome { PartialResult }
else Budget ok
B-->>L: Ok
L->>RL: CheckAsync(state)
alt Rate limited
RL-->>L: Limited(retryAfter)
note over L: wait retryAfter then continue
else Not limited
L->>S: RunAsync(PreModelCall)
S-->>L: Pass / Intervene → SensorInterventionStep
L->>CB: BuildAsync(state, allTools)
CB-->>L: ContextBuildResult(messages, selectedTools)
L->>M: CallAsync(messages, toolDefinitions)
M-->>L: ModelResponse
L->>S: RunAsync(PostModelCall)
S-->>L: Pass / Intervene → SensorInterventionStep
alt No tool calls in response
L->>S: RunAsync(PreReturn)
S-->>L: Pass / Intervene → SensorInterventionStep
L-->>C: AgentOutcome { Done, FinalAnswer }
else Tool calls requested
loop Each tool call
L->>S: RunAsync(PreToolCall)
alt Sensor intervenes
S-->>L: Intervene → SensorInterventionStep + ToolCallStep(IsError)
else Sensor passes
L->>R: DispatchAsync(call)
R-->>L: ToolResult
L->>S: RunAsync(PostToolCall)
end
end
end
end
end
end
```
Budget 耗尽不是异常 —— `IBudgetEnforcer.Check` 会返回 `Exhausted(reason)`,并且循环会在禁用工具的情况下进行最后一次模型调用,
返回 `AgentOutcome { Status = PartialResult }`。`BudgetExceededException` 专用于从循环下层破坏预算的工具或 sub-agent。
## Budget 强制执行
每次运行都受到 `Budget` 的限制 —— 这是在每轮开始时、在任何 Sensor 或模型调用之前检查的四个硬性限制:
| 限制 | 控制内容 |
|---|---|
| `MaxTurns` | 最大循环迭代次数 |
| `MaxTotalTokens` | 整个运行期间累计 token 上限(包括所有模型、工具、Sensor 和 compaction 调用)。不是单轮的上下文窗口 —— 那由 `CompactionOptions.WindowTokens` 控制。 |
| `MaxCost` | 最大支出(基于模型客户端的成本跟踪) |
| `MaxWallClock` | 从第一轮开始的最大流逝时间 |
Budget 耗尽**不是异常** —— 它是控制流。当达到限制时,
循环会在禁用工具的情况下进行最后一次模型调用,以便模型可以根据它已知的信息做出最大努力的回答,
然后返回 `AgentOutcome { Status = PartialResult }`。这保持了 Agent 的可组合性 —— 调用者
总是能得到结果,永远不会从框架本身收到未处理的异常。
```
var outcome = await agent.RunAsync(task, budget: new Budget
{
MaxTurns = 10,
MaxTotalTokens = 100_000,
MaxCost = 0.50m,
MaxWallClock = TimeSpan.FromMinutes(2)
});
if (outcome.Status == AgentStatus.PartialResult)
// The agent hit a limit — outcome.FinalAnswer is its best-effort response.
```
实现 `IBudgetEnforcer` 并通过 `builder.WithBudgetEnforcer()` 注册以替换
默认策略 —— 这对于动态限制、按用户配额或成本分配非常有用。
## 开箱即用
这里的大多数功能都是由上述两种模式 —— Guide、Sensor 或工具 —— 构建的,且每一个都是可选的。三个实验性功能在 **[FEATURES.md](docs/FEATURES.md)** 中有详细说明;所有功能的配置方式都在 **[EXTENDING.md](docs/EXTENDING.md)** 中。
**安全性与循环控制**(Sensor)
- `PromptInjectionSensor` —— 扫描工具结果和用户 turn 中的注入模式(默认开启)
- `PiiRedactionSensor` —— 拒绝泄露 PII 的响应并强制重试
- `StuckDetector`、`MonologueLoopSensor`、`AlternatingToolLoopSensor`、`ToolErrorLoopSensor` —— 捕获无进展和死循环的失败模式
- `ProgressCheckSensor`(任务完成引导)、`ToolResultSanityCheckSensor`(不合理输出)、`CriticSensor`(在 `PreReturn` 处的质量质疑)
- **[基于 AI 的 Sensor](docs/FEATURES.md#ai-powered-sensors-experimental)** —— 将细微的检查(语气、策略)委托给小模型,并根据运行情况计算预算
- **[Taint tracking](docs/FEATURES.md#prompt-injection-and-taint-tracking-experimental)** —— 一旦 trajectory 中存在不受信任的内容,即阻止特权操作
**Memory 与学习**
- **[Agent 学习/技能](docs/FEATURES.md#agent-learning-experimental)** —— Agent 在多次运行间编写并重新加载自己的 `SKILL.md` 流程
- `IMemoryStore` —— 检索增强上下文,由最新的用户 turn 进行查询
- 增量 **compaction** —— 随着历史的剔除折叠出滚动摘要,从而保持成本平稳
**模型与生产环境**
- 模型适配器:**Anthropic**、**Azure OpenAI / AI Foundry** 和 **Ollama**(本地推理) —— 外加 prompt caching 和 circuit-breaker 韧性机制
- **OpenTelemetry** GenAI spans + metrics、**checkpoint/resume**、**human-in-the-loop**(异步挂起/恢复)和 **sub-agents**(每个都有自己的模型、Sensor 和 Budget)
## 架构与配置
### 三层架构
该框架分为三层。如果你在此基础上构建平台或共享 Agent 库,这也是我们推荐的模式。
**第 1 层 —— Port 与核心循环**(`Framework`):循环、所有 Port 接口以及 no-op 默认实现。零基础设施依赖 —— 框架配合你配置的任何适配器运行。这是其他所有部分构建的稳定核心。
**第 2 层 —— 常用适配器**(`Infrastructure.*` 包):框架 Port 的现成实现 —— 模型客户端、tracing、持久化、韧性机制等。使用者根据需要选择包;每个包都是独立的。如果内置适配器不合适,可以通过直接实现 Port 来替换它。
**第 3 层 —— 标准 Agent**(`Infrastructure` 中的 `AddStandardModelHarness`):将常用适配器预配置为合理的、开箱即用的体验。不想做出每一个配置决策的工程使用者可以调用此方法,只需提供一个模型、他们的工具以及任何覆盖配置即可。先应用默认值;你添加的任何内容都会叠加在默认值之上。
### 默认配置了什么
`AddModelHarness`(核心,在 `Framework` 中)和 `AddStandardModelHarness`(在 `Infrastructure` 中)都注册了相同的 **框架默认值**:核心循环和 `Agent`、完整的 Guide 流水线(`HarnessInstructionsGuide → ReActGuide → MemoryGuide → ToolSelectorGuide → ToolCatalogueGuide → SkillsGuide → PinnedContextGuide`,`HeadEvictionTrajectoryGuide` 始终在最后)、`DefaultBudgetEnforcer`、默认的上下文 builder / Guide runner / Sensor runner,以及所有剩余 Port 的 no-op 实现(`NullMemoryStore`、`NullSkillStore`、`PassthroughToolSelector`、`NullCompactionStrategy`、`NullCheckpointStore`、`NullRateLimiter`、`NullHumanNotifier`)。
**两者都不注册模型客户端** —— 你始终需要在 `configure` 回调中提供一个:像 `.WithClaudeModel(...)` 这样的提供商便捷方法、通用的 `.WithModel(...)` 或 `.WithResilientModel(...)`(添加生产级 circuit breaker)。
`AddStandardModelHarness` 随后将带有倾向性的额外配置叠加在上面:
| 接缝 | `AddModelHarness` (基础) | `AddStandardModelHarness` 添加的内容 |
|---|---|---|
| 工具注册表 | `NullToolRegistry` (空) | `InMemoryToolRegistry` |
| 内置工具 | 无 | `GetDateTimeTool` |
| Sensor | 无 | `StuckDetector`、`ProgressCheckSensor`、`PromptInjectionSensor` |
| Tracing | `NullTracer` | `OpenTelemetryTracer` |
Port 默认值使用 `TryAdd`,因此在你的回调中匹配的 `.WithX(...)` 会替换它们;工具、Sensor 和 Guide 是累加的,因此你添加的内容会与内置的一起运行。除了标准集之外的所有内容都是可选的,并需要显式配置 —— `CriticSensor`、循环检测器(`MonologueLoopSensor`、`AlternatingToolLoopSensor`、`ToolErrorLoopSensor`)、`TaintTrackingSensor`、`AiCompactionStrategy`、HITL 和 checkpoint/resume —— 参见 [EXTENDING.md](docs/EXTENDING.md)。
### 包
每一层都作为独立的 NuGet 包发布 —— 只取你所需:
```
dotnet add package SapphireGuard.ModelHarness # core loop + port interfaces
dotnet add package SapphireGuard.ModelHarness.Infrastructure # sensors, tracing, DI wiring
dotnet add package SapphireGuard.ModelHarness.Anthropic # Claude adapter
dotnet add package SapphireGuard.ModelHarness.AzureOpenAI # Azure AI Foundry / Azure OpenAI adapter
dotnet add package SapphireGuard.ModelHarness.Ollama # Ollama adapter (local inference)
dotnet add package SapphireGuard.ModelHarness.Resilience # Polly retry + circuit breaker
dotnet add package SapphireGuard.ModelHarness.Persistence # checkpoint / resume
```
在 [`getting-started/`](getting-started/) 中有一个可运行的入门项目 —— 打开 `GettingStarted.slnx`,放入一个包含你 API Key 的 `appsettings.local.json`,然后运行。
### 会话式 Agent
上面的入口点运行任务直到终止状态。对于**多轮对话 Agent** —— 一个在多次用户 turn 中保持开启状态的 Agent —— 请使用 `AddChatHarness`(基础版,在 `Framework` 中)或 `AddStandardChatHarness`(带有倾向性,在 `Infrastructure` 中)。相同的循环、状态和 `Agent`;它们只是为会话生命周期替换了两个接口缝:**按 turn 计算的 Budget**(`TurnScopedBudgetEnforcer`,这样每个用户 turn 都会获得新的配额,而不是整个对话耗尽一个 Budget)和**未固定的目标**(trajectory guide 停止将第一条消息重新注入为 `[ORIGINAL GOAL]`,因为对话的实时目标是最近的 turn)。
`AddStandardChatHarness` 还配置了适合对话的 Sensor —— `PromptInjectionSensor` 和 `StuckDetector` —— 但没有配置用于任务完成的 `ProgressCheckSensor`。
通过使用 `WithUserMessage` 传回先前结果的状态来推进对话:
```
services.AddStandardChatHarness(builder => builder
.WithSystemPrompt("You are a friendly assistant.")
.WithClaudeModel(new ClaudeClientOptions { ApiKey = apiKey }));
await using var provider = services.BuildServiceProvider();
var agent = provider.GetRequiredService();
var time = provider.GetRequiredService();
AgentOutcome? outcome = null;
while (Console.ReadLine() is { Length: > 0 } input)
{
var state = outcome is null
? AgentState.NewTask(input, budget, time.GetUtcNow()) // first turn
: outcome.FinalState.WithUserMessage(input, time.GetUtcNow()); // continue the conversation
outcome = await agent.RunAsync(state);
Console.WriteLine(outcome.FinalAnswer);
}
```
参见 `samples/Conversation`(基础对话 REPL)和 `samples/ChatSubAgent`(委托给 sub-agent 专家的对话 Agent)。
## 项目的由来
我构建这个项目是为了了解 Agentic 系统到底是如何工作的 —— 不是通过阅读关于循环的文章,而是通过实现它 —— 然后有一些具体的东西来教导我的团队。这就是为什么每一个控制点都是一个命名的 Port,以及为什么 [PRIMER.md](docs/PRIMER.md) 解释的是理念而不仅仅是 API:框架和解释是一起编写的。
它之所以公开,是因为有其他人的参与能让学习走得更远。如果这里有什么是错误的、遗漏的,或者你会有不同做法的决策,请[提出 issue](https://github.com/AnilKerai/model-harness/issues) —— 我宁愿听到它也不想错过,而且直白坦诚的意见是最有用的。[CONTRIBUTING.md](CONTRIBUTING.md) 介绍了如何提交更改。
## 链接
- [getting-started/](getting-started/) —— 使用已发布 NuGet 包的最小可运行项目
- [RUNNING.md](docs/RUNNING.md) —— 每个样例的配置和运行说明
- [EXTENDING.md](docs/EXTENDING.md) —— 每个扩展点的代码实操指南
- [FEATURES.md](docs/FEATURES.md) —— 实验性功能(学习、AI Sensor、Taint tracking)的深度解析
- [PRIMER.md](docs/PRIMER.md) —— 关于框架背后 Agentic-AI 理念的入门:循环 / 上下文工程和 Agentic 原语
- [GLOSSARY.md](docs/GLOSSARY.md) —— 所有框架术语的定义
- [ROADMAP.md](docs/ROADMAP.md) —— 已完成和待实现的内容
- [FAQ.md](docs/FAQ.md) —— 设计决策常见问题解答
标签:AI智能体, AI框架, DLL 劫持, PII检测, 上下文工程, 大语言模型, 提示词注入检测, 用户代理