智能体工具契约

Known.Platforms.IAgentService 之上的工具(Function Calling)契约:[AgentTool] 特性、IToolRegistry、AgentToolDescriptor 三件套落在开源侧,具体运行时(扫描 / 参数 schema 生成 / 反射执行环)由宿主或 Known.AgentKit 插件承担。

最后更新

状态:契约已落地,工具运行时已在 AgentKit M2 打通,会话与治理落库已在 M3 落地。 本篇描述的 IAgentService / [AgentTool] / IToolRegistry 三个类型都在开源框架 Known 里(Known.Ai 命名空间),宿主可以直接依赖它们写自己的智能体实现;闭源插件 Known.AgentKit 已交付多厂商路由 / 降级(M1)、工具执行环(M2,含 [AgentTool] 扫描、参数 JSON Schema 自动生成、反射调用与结果回灌)与会话账本 / 成本 / 审计落库(M3,SysAgentTurn 用量账本 + SysAgentAudit 审计事件)。评测跑批(IEvalRunner)、Prompt 版本仓库(IPromptStore)、请求管道装饰器链与 SchemaContextEnricher 仍待后续。若你只需要一个能自洽回话、还能调工具的智能体抽屉,读完本篇即可上手。


为什么要单独立一个「工具」契约

框架顶栏的智能体抽屉(AgentDrawer)本身只是个壳:输入框、气泡、发送 / 停止按钮。真正把"能查询当前用户的订单""按状态筛选设备"这类业务能力暴露给模型的,是工具——模型读一段自然语言,产出一个 {name, arguments} 结构,运行时按这个名字把参数交给宿主的一段 C# 方法,把返回值再喂回模型。这条链路在业内叫 Function Calling / Tool Use。

如果框架只给一个 IAgentService 大契约、让宿主整个端出来,工具清单、参数 schema、鉴权与重试这些通用麻烦每家都要重写一遍。所以框架侧把这四件事拆出来:

  • IAgentService(Known.Platforms):一帧请求 → 一帧回答。宿主自己决定接大模型、接 known-ai 侧车、还是先回固定话术。
  • [AgentTool](Known.Ai):方法级特性。挂上去就是"这个方法是一枚工具",名字、描述、分类、是否隐藏都在特性里。
  • IToolRegistry(Known.Ai):注册与查询。运行时把当前程序集里所有带 [AgentTool] 的方法聚合成一张表,交给模型看。
  • AgentToolDescriptor(Known.Ai):位置记录,注册表的行形状;Name / Description / Category / DeclaringType / MethodName / Hidden / Parameters 七个字段,Parameters 是扫描器生成的 OpenAI function.parameters JSON Schema(缺省时适配器退化成只有 name/description)。

契约留在开源,具体实现在闭源或宿主——这与 [Hook] / [Menu] / [Table] 的分工同构:声明在开源、实现在闭源或宿主。宿主没有 AgentKit 也能上生产,只是要自己写"扫描 / 参数 schema 生成 / 反射执行环"这些运行时。

最小实现:三段代码把工具跑起来

[AgentTool] 是方法级特性。Config.AddModule(typeof(X).Assembly) 把宿主程序集登记进 Config.Assemblies;AgentKit 的 DefaultToolRegistry 在首次取用时懒扫这些程序集、把带 [AgentTool] 的公有实例方法聚合成表。以"给智能体一枚查订单的工具"为例:

using Known.Ai;
using Known.Models;
using Known.Platforms;

[Service]
public class OrderAgentService(IOrderRepository repo)
{
    [AgentTool("query_orders", Description = "按用户与状态查询订单", Category = "订单")]
    public async Task<string> QueryOrders(string userId, string status)
    {
        var list = await repo.FindByUserAsync(userId, status);
        return string.Join(";", list.Select(o => $"{o.Id} · {o.Total:F2}"));
    }
}

宿主只需 AddAgentKit() + Config.AddModule(...),不必自己写 SendAsync:AgentKit 的 AgentKitService 会在组装 ChatRequest 时把 IToolRegistry.All(滤掉 Hidden)灌到 Tools,模型决定调用就回一个 ToolCall { Id, Name, Arguments },DefaultToolExecutor 从注册表找到 DeclaringType + MethodName、在请求作用域内实例化 OrderAgentService、反射执行,把结果作为一条 ChatRole.Tool 消息连同 assistant 的调用意图一并回灌、再问一轮。整个过程直到模型不再调用工具、或触顶 MaxToolLoops(默认 5,防失控)。

若宿主不用 AgentKit、自写 IAgentService,就自己实现"扫描 → 下发候选 → 解析 tool_calls → 反射执行 → 回灌"这段环;开源侧只提供契约与描述符,不绑架实现。

工具执行环(M2 已实现)

AgentKit 把 Function Calling 的完整往返收在四处,宿主一行配置即得:

  • 扫描:DefaultToolRegistry 懒扫 Config.Assemblies 里的 [AgentTool] 方法,同名以最后登记者为准(幂等);ReflectionTypeLoadException 时按可用类型继续、不整体崩。
  • 参数 Schema 自动生成:ToolSchemaBuilder 把方法签名翻译成 {type:object, properties, required}——标量与标量数组到位,复杂对象浅标 type:object;CancellationToken 参数跳过、带默认值或不必填的不进 required。生成的 Schema 存进 AgentToolDescriptor.Parameters,由 OpenAiAdapter 原样内联进 tools 定义。
  • 反射执行:DefaultToolExecutor 按参数名把模型给的弱字典实参(JsonElement)强绑到形参类型,支持同步返回值、Task / Task<T> / ValueTask / ValueTask<T>;按声明返回类型判定有无结果(避开编译器 Task<VoidTaskResult> 把 void 误当空对象的坑)。
  • 异常即回灌:单个工具抛异常不外泄成整轮失败,而是折成一条 {"error":"…"} 的工具消息喂回模型,让它自纠或改口;唯有用户点「停止」触发 CancellationToken 才原样上抛、立刻收场。

MaxToolLoops 触顶且模型仍只要工具不给结论时,AgentKitService 回一句说明而非空气泡;AgentKitOption.EnableTools = false 可整体关掉候选下发与执行、退回纯问答。

会话与治理落库(M3 已实现)

智能体每答一轮,要留下两件东西:给用户回看的会话气泡,和给运维 / 合规用的治理账本。M3 的取舍是把二者彻底分开、避免双写——

  • 气泡正文的唯一真相源是 IAgentStore 契约,AgentKit 不重造会话、不复制正文。开源侧自带 MemoryAgentStore(进程内、重启即清空)托底演示;把会话真正落库(SysAgentSession / SysAgentMessage 两张 [Table] 实体)也是 AgentKit 的增量——它带 [Service] DbAgentStore,Config.AddModule(typeof(AgentKitMarker).Assembly) 后以同名契约「最后登记者优先」接管 IAgentStore、让历史跨重启留存。治理账本只读会话、不另存一份正文。
  • 关联键走框架 PR:AgentRequest 新增 ConversationId;抽屉在派发首轮之前先向 IAgentStore.NewSessionAsync 建一个空会话壳子、拿回服务端分配的主键回填本字段,于是 AgentKitService 在应答当口就能把这一轮关联到一个真实存在的会话,而不是等收尾写气泡时才拿到 id。
  • 薄治理账本(SysAgentTurn,一轮一条)只落会话存储没有、也不该有的治理维度:token 用量(跨工具环各次往返累加)、金额、实际应答的适配器与模型(降级链最终落点,常 ≠ 请求点名的模型,由 DefaultChatGateway 在命中候选时 stamp、OpenAiAdapter 从应答体读出)、工具调用轨迹、端到端耗时、终止状态。回看正文时按 ConversationId join 会话消息表即可。
  • 逐事审计(SysAgentAudit,一事一行)记 request / tool_call / tool_result / error / stop,按 TurnId 归属某一轮、按 Seq 在本轮内定序;主键 / 时间戳 / 用户与租户归属交框架 EntityBase 落库时盖章。审计独立于气泡存储——即使用户删了会话,掩码后的事件流仍按 RetentionDays 留存供回放。
  • 成本聚合(ICostMeter.SummarizeAsync)因用量已折叠进 SysAgentTurn,只做只读聚合、不再单独记账。

AgentKitService.SendAsync 一开始就预铸 turnId 作账本主键(供同轮审计指回),收尾在 finally 里用 CancellationToken.None 落库——用户点「停止」也要把这一轮记全;治理服务一律从请求作用域 GetService 取用、解析不到即静默跳过(只跑开源 MemoryAgentStore 的纯内存会话照常应答、不出账本),且账本与审计各写各的、任一失败都吞并留痕,绝不反噬用户应答。四张表——会话与气泡的 SysAgentSession / SysAgentMessage、治理账本的 SysAgentTurn / SysAgentAudit——都随宿主 Config.AddModule(typeof(AgentKitMarker).Assembly) 进入扫描,由框架 EnsureSchemaAsync 非破坏式建表增列。裁剪器 AgentKitRetentionService.TrimAsync 只删过期的账本 / 审计,绝不越权动用户可见的会话历史——定时策略交宿主接进自己的调度设施,AgentKit 作为类库不引入 Microsoft.Extensions.Hosting。

治理后台页(M3 已实现)

账本落库后,运维与合规要有地方看。AgentKit 不新造 UI——Known.AgentKit 改为 Razor 类库后,用「运行时 Schema 渲染」范式交付三页(与「生成建模」产出的页面同一套运行时,框架内置的跟踪日志一类的低容量运维页则改用手写 Razor):每个页面只是一句 <SchemaPage Key="…" />,列 / 单元格 / 分段 / 检索 / 指标全写在随程序集内嵌的 *.schema.json 里,取数与聚合由标了 [Provider] 的只读数据源从治理表供给,不生成任何页面代码。三页都挂在右上角「开发中心」分组、Roles="超级管理员" 门禁:

  • 用量账本 /dev/agent/turns(AgentTurnProvider over SysAgentTurn):一轮一条的账本检索,可按状态(成功 / 失败 / 已停止)分段、按模型 / 适配器 / 用户 / 会话 / 工具关键字过滤,头部内嵌「总请求数 / 输入 Token / 输出 Token / 总成本」指标卡;点开详情看工具清单与失败原因。正文不落此表,回放按 ConversationId join 会话消息表。
  • 审计回放 /dev/agent/audit(AgentAuditProvider over SysAgentAudit):一事一行的事件流,按时间倒序、本轮内按 Seq 定序,可按动作(请求 / 工具调用 / 工具结果 / 护栏拦截 / 停止 / 错误)分段,点开详情看掩码后的 DataJson 载荷,供事后安全合规回放。
  • 成本仪表盘 /dev/agent/cost(AgentCostProvider,纯聚合、无表格无写):栅格看板——用量总览指标卡 + 成本·按模型 / 请求数·按用户 / 输入 Token·按模型 三张柱图,是 ICostMeter 读侧聚合的可视化落点。

装配前提与其它 AgentKit 能力一致:宿主须 Config.AddModule(typeof(AgentKitMarker).Assembly) 把本程序集纳入 Config.Assemblies(Schema 注册表据此扫到内嵌包、Provider 解析器据此装配、路由树据 App.razor 的 AdditionalAssemblies 挂上 @page),并 services.AddAgentKit(...) 登记治理数据源。三页的英文词条并入 Known.AgentKit/wwwroot/i18n/en.json,与框架包按语言 code 幂等合并。

与 CRUD [Hook] 的边界

[Hook] 与 [AgentTool] 都挂在方法上,很容易混。它们回答的是两个不同问题:

[Hook] [AgentTool]
触发时机 一次数据 CRUD 的前后(OnSaving / OnSaved / OnDeleting / OnDeleted) 一次对话推理里,模型主动请求
输入形状 实体实例(框架知道类型) 弱字典 IDictionary<string, object>(JSON 反序列化而来)
谁在决定要不要调 框架在写库前/后自动遍历 大模型自己按名字选
返回值消费方 框架(可改写实体 / 中断写入) 下一轮对话的 prompt

AgentKit 明确不复用 [Hook] 来做智能体扩展——一份代码同时绑数据写入与对话组装会制造语义漂移;见草案 §3。

什么留在开源、什么留给宿主或 AgentKit

开源 Known 侧提供(本契约即其一部分):AgentDrawer 壳、IAgentService 契约、IAgentStore 契约 + 开箱即用的默认实现 MemoryAgentStore(进程内、按用户隔离、重启即清空)与可移植 AgentSession DTO、AgentModels(AgentRequest / AgentReply / AgentMessage / AgentCard / AgentFile / AgentContextItem)、AiModelBar、[AgentTool] / IToolRegistry / AgentToolDescriptor。这些够一个懂大模型的宿主自己把智能体跑起来、会话也能在运行期间回看。

闭源 Known.AgentKit 已交付的增量:多厂商 IModelAdapter 与 IChatGateway 的路由/降级(M1)、[AgentTool] 扫描 + 参数 Schema 生成 + 反射执行环(M2)、会话气泡落库 DbAgentStore(接管 IAgentStore,落成 SysAgentSession / SysAgentMessage 两张表、历史跨重启留存)+ 治理账本 IConversationStore + 成本聚合 ICostMeter + 审计落点 IAuditSink(M3,治理侧另落 SysAgentTurn / SysAgentAudit 两张表)与治理后台三页(用量账本检索 / 审计回放 / 成本仪表盘,运行时 Schema 渲染、不新造 UI)。仍待补齐的治理层:请求管道装饰器链(缓存 → 限流 → 预算护栏 → 审计 → Meter → 推理)、Prompt 版本仓库(IPromptStore)、评测跑批(IEvalRunner)、上下文注入器(IContextEnricher,杀手锏是 SchemaContextEnricher 把 SchemaRegistry 里的实体 / 字段 / 关系摘要自动塞给模型)。这些不是"没它就跑不起来",是"要有治理与可观测性就得写"的那一层。

相关文档

  • 智能体抽屉:顶栏入口的启用、关闭与自定义气泡样式,也是 [AgentTool] 服务要接的宿主壳。
  • 模块与插件式装配:Config.AddModule 如何把程序集登记进 Config.Assemblies(供 [AgentTool] 扫描)。
  • 数据层:[Hook] 的 CRUD 语义与所在链路,用来对照 [AgentTool] 的对话语义。

已知未决

工具级鉴权(谁能调哪些工具)刻意没进契约,留给 AgentKit 装饰器链或宿主自己在方法体里判 IAuthProvider;参数 Schema 目前是"浅映射"(复杂对象只标 type:object 不展开字段),若需强类型嵌套 Schema 可给宿主自定义 IToolRegistry / IToolExecutor 覆盖(AddAgentKit 用 TryAdd 语义,宿主先登记者优先)。known-ai 侧车当前 /chat 还不吐 tool_calls,走本机模型时工具环会干净降级(无候选即纯问答)。

微信公众号二维码 扫码关注公众号
QQ 交流群二维码 扫码加入 QQ 群