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,也可以在无需改动循环的情况下替换其中的任何单个组件。 [![NuGet](https://img.shields.io/nuget/v/SapphireGuard.ModelHarness.svg)](https://www.nuget.org/packages/SapphireGuard.ModelHarness) [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/AnilKerai/model-harness/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) ![.NET](https://img.shields.io/badge/.NET-8.0%20%7C%2010.0-512BD4) *我写这个项目是为了通过构建一个系统来学习 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检测, 上下文工程, 大语言模型, 提示词注入检测, 用户代理