工具与知识库
这一页讲怎样让模型「做事」和「有依据」:Expose.Ai 把已有的操作契约变成模型可调用的工具,工具经完整管道执行、失败以 JSON 交给模型;知识库流水线把文档切块、嵌入、写入向量存储,检索后拼进提示词;AI.VectorData 把 IKnowledgeStore 适配到 Microsoft.Extensions.VectorData 的任何连接器,并自带健康项;最后说明它与 MCP 的关系。读完后,我们能让上一页的助手调用订单查询、并按 FAQ 文档回答问题。前置知识是模型与对话、操作契约与执行管道。
为什么需要
模型只会说话,查订单、改状态要靶向到业务代码;给它随手写一堆函数,权限、校验、事务、审计全都绕过了,模型能做的事超过了提问的用户本人。框架把工具与操作契约合二为一:操作上加 Expose.Ai,工具目录自动把它桥接成模型工具,调用时在新的调用上下文里经完整管道执行,沿用调用方的主体与租户。回答要有依据时,知识库流水线把检索到的片段编号拼进提示词,存储由应用选的向量库承担,框架不伪造默认实现。
概念
| 术语 | 说明 |
|---|---|
工具目录 IAIToolCatalog | 合并显式登记的 AITool 与 Expose.Ai 的操作;All() / Find(name) 返回声明,Bind(caller) 返回绑定到调用方、可执行的工具 |
OperationAIFunction | 一个操作桥接成的工具:名字、描述、输入与输出 Schema 来自操作契约 |
AIToolsFeature | 提供 ai.tools@1 的默认实现,Profiles.Full 已包含;启动时构建目录并校验工具名 |
IKnowledgeIngestor / IKnowledgeRetriever / IRagPromptAugmenter | 知识库流水线的三个服务:摄入与删除、检索、提示增强,在 AI.Abstractions 里,与存储无关 |
IKnowledgeStore | 能力 ai.knowledge@1 的契约:写入、按文档删除、向量搜索;由 AI.VectorData 实现 |
RetrievedChunk | 检索结果的一块:Id、DocumentId、ChunkIndex、Text、Score、Metadata |
最小示例
把订单查询操作暴露成工具,并在特性里登记护栏与一个显式工具。先是操作:
using XiHan.Framework.Kernel.Operations;
using XiHan.Framework.Primitives;
namespace MyApp.Orders;
public sealed record ListOrdersRequest(string? Status);
public sealed record OrderSummary(string Id, string Status, decimal Amount);
public sealed record OrderList(IReadOnlyList<OrderSummary> Orders);
// Expose.Ai:工具目录把它桥接成名为 orders_list 的模型工具
[Operation("orders.list", Expose = Expose.Http | Expose.Ai)]
public sealed class ListOrders : IOperation<ListOrdersRequest, OrderList>
{
private static readonly OrderSummary[] _orders = [new("o-1", "pending", 12.5m), new("o-2", "shipped", 99m)];
public ValueTask<Result<OrderList>> HandleAsync(ListOrdersRequest request, InvocationContext context)
{
var matched = _orders.Where(order => request.Status is null || order.Status == request.Status).ToList();
return ValueTask.FromResult(Result.Success(new OrderList(matched)));
}
}OrdersFeature 以 context.AddGeneratedOperations() 登记它,写法见审计;三个请求与响应类型登记在项目的 JsonSerializerContext 里。然后是助手特性,声明对聊天与工具能力的依赖,登记上一页的护栏与一个用 AIFunctionFactory 创建的显式工具:
using Microsoft.Extensions.AI;
using XiHan.Framework.AI.Abstractions;
using XiHan.Framework.Kernel;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Primitives;
using XiHan.Framework.Primitives.Manifests;
namespace MyApp.Assistants;
public sealed class AssistantsFeature : IFeature
{
public Manifest Manifest { get; } = new()
{
Id = "xihan.myapp.assistants",
Package = "MyApp",
Version = "1.0.0",
Primitives = 1,
Stability = Stability.Experimental,
Kind = ManifestKind.Capability,
Requires =
[
new CapabilityRequirement(KernelCapabilities.Hosting, KernelCapabilities.Version),
new CapabilityRequirement(AICapabilities.Chat, AICapabilities.Version),
new CapabilityRequirement(AICapabilities.Tools, AICapabilities.Version),
],
Entry = typeof(AssistantsFeature).FullName,
};
public void Configure(FeatureContext context)
{
context.Services.AddAIGuardrail<QuestionLengthGuardrail>();
context.Services.AddAITool(AIFunctionFactory.Create(static () => DateTimeOffset.UtcNow.ToString("O"), "clock_now", "Current UTC time in ISO 8601."));
context.AddGeneratedOperations();
}
}最后在上一页的 AskAssistant 请求里加一行 Tools = ["orders_list", "clock_now"]。运行后:问「有哪些待发货的订单」时,模型发起 orders_list 调用、参数 {"status": "pending"},客户端的函数调用循环在管道里执行 orders.list 并把 {"orders":[{"id":"o-1",…}]} 交回模型,最终回答里出现 o-1;审计与追踪里能看到这次 orders.list 调用与发起聊天的调用共享同一个 trace-id。
操作工具
工具目录合并两处来源:AddAITool(AITool) 显式登记的工具原样返回;登记表里 Expose 含 Ai、稳定性不是 Stability.Internal 的操作,各桥接成一个 OperationAIFunction。IAIChat 会自动把 Tools 列出的名字绑定到当前调用。
| 项 | 规则 |
|---|---|
| 名字 | <resource>_<verb>,- 与 . 换成 _;超过 64 个字符时取前 55 个字符加 _ 与 SHA-256 的前 8 位十六进制;XiHan:AI:Tools:<操作 ID> 覆盖 |
| 名字校验 | 必须匹配 ^[A-Za-z0-9_-]{1,64}$,不合法或重名时启动失败(AI_VALIDATION_001):AIToolsFeature 与 OpenAIFeature 启动时都构建工具目录,只装其中任何一个也一样 |
| 描述 | 消息目录里的 <操作 ID>.description(ID 里的 - 换成 _),按调用方区域翻译;没有时为空 |
| 输入 Schema | 操作的请求 Schema,没有时 {"type":"object"} |
| 输出 Schema | 操作的响应 Schema |
工具被调用时,参数序列化成请求 JSON,在新作用域、新 InvocationContext 里经完整管道执行:trace-id、截止时间、Baggage 与安全上下文(主体与租户)复制自调用方,Validate、Authorize、Transaction 等阶段照常生效;新上下文不带调用来源,GetOrigin() 为无值。成功返回响应 JSON;失败返回 {"error":"<错误码>","message":"<按调用方区域翻译的文案>"} 交给模型处理,不抛异常。AI.OpenAI 的聊天客户端带函数调用循环,模型发起的工具调用在客户端里自动执行。
工具以调用方身份执行
工具调用沿用调用方此刻的主体与租户:调用方已在自己的管道里认证过,凭据不复制,Authenticate 阶段没有凭据可认证即保留这个主体。Authorize 阶段以调用方主体按契约判定,契约声明了 Permission 的操作,调用方持有该权限才放行;没有权限得到 SECURITY_DENIED_004、匿名调用得到 SECURITY_DENIED_001,错误以 JSON 交给模型。模型能做的事不超过发起聊天的用户本人能做的事。
知识库
知识库流水线是「切块 → 嵌入 → 写入存储 → 检索 → 提示增强」。切块、嵌入、检索与增强在 AI.Abstractions 里,与存储无关;Provider 只实现 IKnowledgeStore 的写入、按文档删除与向量搜索。
| 服务 | 作用 |
|---|---|
IKnowledgeIngestor | IngestAsync(requests) 批量摄入,每篇文档一个 Result<int>(写入的块数);RemoveAsync(collection, documentIds) 按文档删除 |
IChunkingStrategy | 内置 FixedWindowChunkingStrategy:按字符的固定窗口,重叠裁到 [0, MaxChunkSize − 1],换行统一为 \n,空白块丢弃 |
IKnowledgeRetriever | RetrieveAsync(query):问题嵌入 → 向量搜索 → 按 MinScore 过滤 |
IRagPromptAugmenter | Augment(question, chunks):把检索结果编号拼进提示词,返回 string(模板在启动时校验过,增强本身不会失败) |
using Microsoft.Extensions.AI;
using XiHan.Framework.AI.Abstractions.Chat;
using XiHan.Framework.AI.Abstractions.Knowledge;
using XiHan.Framework.Primitives;
namespace MyApp.Assistants;
public sealed class FaqKnowledge(IKnowledgeIngestor ingestor, IKnowledgeRetriever retriever, IRagPromptAugmenter augmenter, IAIChat chat)
{
public async ValueTask<IReadOnlyList<Result<int>>> ImportAsync(IReadOnlyDictionary<string, string> documents, CancellationToken cancellationToken)
{
// 批量摄入:每篇文档一个结果(写入的块数),同一文档重新摄入先写新块、再删多余的旧块
var requests = documents
.Select(static document => new KnowledgeIngestRequest
{
DocumentId = document.Key,
Text = document.Value,
Metadata = new Dictionary<string, string> { [KnowledgeMetadataKeys.Source] = "faq" },
})
.ToList();
return await ingestor.IngestAsync(requests, cancellationToken);
}
public async ValueTask<Result<string>> AnswerAsync(string question, CancellationToken cancellationToken)
{
var found = await retriever.RetrieveAsync(new KnowledgeQuery { Text = question, Top = 3, MinScore = 0.3 }, cancellationToken);
if (!found.TryGetValue(out var chunks))
{
return found.Error!;
}
var prompt = augmenter.Augment(question, chunks);
var reply = await chat.CompleteAsync(new AIChatRequest { Messages = [new ChatMessage(ChatRole.User, prompt)] }, cancellationToken);
return reply.Map(static response => response.Text);
}
}FaqKnowledge 以 builder.Services.AddScoped<FaqKnowledge>() 登记(IAIChat 是作用域服务)。流水线的规则:
- 记录 ID 是
<documentId>#<chunkIndex>。同一文档重新摄入先写后删:新块全部嵌入、校验维度并写入成功后,才删掉旧版本多出来的块(块序号不小于新块数的),同 ID 的块由写入覆盖。嵌入、维度校验或写入失败时不删任何旧块,旧文档照常可检索;写入中途失败最多留下新旧混合的块,按返回的错误重试摄入即恢复一致。删多余旧块这一步失败时返回该错误,新块已经在库里。新文本切不出块时整篇删除,返回 0。 - 每个嵌入向量的长度必须等于
XiHan:AI:Knowledge:Dimensions,不一致返回AI_VALIDATION_002。换嵌入模型时一并改维度,并换一个集合。 - 摄入与检索的嵌入 Provider 依次取请求的
Provider、XiHan:AI:Knowledge:EmbeddingProvider、目录的DefaultEmbeddingProvider,都没有时返回AI_NOTFOUND_001(ai.provider.embedding_default_missing)。 - 检索的
Top缺省取XiHan:AI:Knowledge:Top;Filter(RetrievalFilter)的DocumentIds与Metadata(键值相等)取交集;MinScore在存储返回后过滤。 Augment在检索结果为空时原样返回问题。配置了XiHan:AI:Knowledge:PromptName就用它指向的模板(占位符{context}、{question},配置里的或AddAIPrompt登记的都行),没配置时用内置英文模板。模板在启动时校验:AIToolsFeature、VectorDataKnowledgeFeature与OpenAIFeature启动时都解析增强器(只装其中任何一个也一样),模板不存在(AI_NOTFOUND_002)或引用了这两个以外的占位符、用代表性参数试渲染失败(AI_VALIDATION_001)即启动失败,不会悄悄换成内置模板。{context}里每个片段一段:先是[序号] Source: 来源一行,再是片段文本。来源依次取块元数据的KnowledgeMetadataKeys.Title(title)、KnowledgeMetadataKeys.Source(source),都没有时取文档 ID。文档 ID 多半是 Guid,模型拿它说不清依据,所以摄入时最好带上标题。- 没有装配
ai.knowledgeProvider 时,摄入、删除与检索都返回AI_UNAVAILABLE_002。
AI.VectorData
AI.VectorData 把 IKnowledgeStore 映射到应用登记的 Microsoft.Extensions.VectorData 的 VectorStore,任何连接器都行;容器里没有 VectorStore 时启动失败。向量库连接器由应用选择并自行引用其包,下例用内存连接器,只适合开发:
dotnet add package XiHan.Framework.AI.VectorData --prereleaseusing XiHan.Framework.AI.OpenAI;
using XiHan.Framework.AI.VectorData;
builder.Services.AddSingleton<VectorStore>(new InMemoryVectorStore());
builder.AddXiHan(xihan => xihan
.UseStandardProfile()
.UseFeature<OpenAIFeature>()
.UseFeature<VectorDataKnowledgeFeature>());{ "XiHan": { "AI": { "Knowledge": { "Collection": "faq", "Dimensions": 768 } } } }集合定义、距离函数与过滤的下推方式见 AI.VectorData。它自带健康项 ai.vectordata.store,应用不必另写向量库的健康检查:
- 启动时同步探一次,之后每隔
XiHan:AI:VectorData:HealthProbeIntervalSeconds(缺省 15 秒,也是单次探测的超时)在后台重探。 - 探针只问默认集合(
XiHan:AI:Knowledge:Collection)在不在,存在与否都算连得上;不列集合、不建集合、不读写记录。 - 向量库应答即 Ready。连接器抛出时 Degraded,错误码
AI_UNAVAILABLE_002,与检索在这时返回的码相同;探测超时也是 Degraded,错误码KERNEL_UNAVAILABLE_005。 - 条目 Degraded 时应用照常启动、照常服务,
/health/ready仍通过,恢复后条目回到 Ready。条目不带数据与说明,不写端点或连接信息。汇总见生命周期与健康。
配置
| 键 | 类型 | 缺省 | 说明 |
|---|---|---|---|
XiHan:AI:Tools:<操作 ID> | string | 按约定推导 | 覆盖操作工具名 |
XiHan:AI:Knowledge:Collection | string | knowledge | 默认集合 |
XiHan:AI:Knowledge:Dimensions | int,至少 1 | 1536 | 嵌入维度 |
XiHan:AI:Knowledge:ChunkSize | int,至少 1 | 1000 | 切块大小(字符) |
XiHan:AI:Knowledge:ChunkOverlap | int,不小于 0 | 100 | 相邻块的重叠(字符) |
XiHan:AI:Knowledge:Top | int,至少 1 | 5 | 默认检索条数 |
XiHan:AI:Knowledge:EmbeddingProvider | string | 目录的默认嵌入 Provider | 知识库用的嵌入 Provider;请求写了 Provider 时以请求为准 |
XiHan:AI:Knowledge:PromptName | string | 内置模板 | 提示增强用的模板名;启动时校验存在且只用 {context}、{question} |
XiHan:AI:VectorData:HealthProbeIntervalSeconds | 有限的数值,大于 0 | 15 | AI.VectorData 健康项的重探间隔,也是单次探测的超时 |
Provider、提示词与护栏的键见模型与对话。与那里相同,这些键在装配时读一次、不热更新,取值不合法时抛出 ArgumentException、启动失败。
与 MCP 的关系
Expose.Ai 面向进程内的模型调用:工具在本进程的聊天里被模型发起、在本进程的管道里执行。要让外部的 Agent 或 IDE 通过 Model Context Protocol 调用同一份操作契约,由 Protocols.Mcp 把 Expose.Mcp 的操作暴露成 MCP tool,见 MCP。两者共用操作契约与管道,一个操作可以同时标 Expose.Ai | Expose.Mcp(Expose.All 包含两者),分别服务于「我的应用里的模型」与「别人的 Agent」。
与其他能力的关系
- 执行管道:工具调用是一次完整的管道调用,审计、事务、校验都照常生效,审计事件与发起聊天的调用共享 trace-id,见执行管道与审计。
- 操作契约:
Expose.Ai是Expose的一个标志位,Expose.All包含它,见操作契约。 - 测试:
AI.Conformance提供KnowledgeStoreConformance、AIProviderConformance与确定性的HashingEmbeddingGenerator,测试不依赖外部模型,见一致性套件与门禁。自写目录的应用继承AIProviderConformance,实现StartAsync(被测 Provider 是默认)与StartWithoutDefaultAsync(没有默认),能把聊天与嵌入的默认分开时再覆写StartWithSeparateDefaultsAsync。 - 框架不提供 Agent 工厂,等
Microsoft.Agents.AI稳定后另立规范。
常见问题
怎样控制模型能调哪些工具
两层:操作上没有 Expose.Ai 就不进目录;请求的 Tools 只列这次要带的名字,没列的模型看不到。再往下是授权:工具以调用方身份执行,没有权限的操作返回 SECURITY_DENIED_004 给模型。
工具名为什么是 orders_list 而不是 orders.list
模型工具名只允许字母、数字、_ 与 -,所以 . 换成 _;太长时截断加哈希。要自己定名字用 XiHan:AI:Tools:orders.list,重名或不合法在启动时就失败。
换了嵌入模型,旧的知识库还能用吗
不能直接用:向量维度变了,摄入会报 AI_VALIDATION_002,检索结果也没有意义。改 Dimensions、换一个 Collection,重新摄入全部文档。
摄入中途失败,知识库会不会一半新一半旧
最多在同一文档内留下新旧混合的块;旧块不会被提前删掉,所以旧文档照常可检索。按返回的错误重试摄入即恢复一致。
没有向量库能不能先跑起来
能。开发时用 Microsoft.Extensions.VectorData 的内存连接器登记 VectorStore;完全不装知识库 Provider 时,摄入与检索返回 AI_UNAVAILABLE_002,聊天与工具不受影响。
相关包与下一步
- AI.Abstractions:工具目录、知识库流水线与契约
- AI.OpenAI:带函数调用循环的聊天客户端与嵌入生成器
- AI.VectorData:
IKnowledgeStore的向量库适配与健康项 - Protocols.Mcp:把操作暴露成 MCP tool
- 下一章:插件:隔离级别与清单
