数据截至 (上游 commit 5e1f1fb87d9a)
第 01 章 · 内核与可调用单元:Kernel、KernelFunction、插件
本章讲什么: SK 的最小心智模型只有两个名词——Kernel 是容器 + 调度器,KernelFunction 是唯一的可调用单元。本章把这两个词彻底讲透:Kernel 由哪四块拼成、一次调用的骨架长什么样、函数的元数据是怎么从 Python 签名反推出来的、插件如何充当函数的命名空间与来源。
引用路径约定: 本章所有
file:line均相对克隆里的python/semantic_kernel/目录。例如kernel.py:62指python/semantic_kernel/kernel.py第 62 行。
1.1 先建直觉:整个框架 就两个名词
Kernel 不是"引擎",它是一本通讯录 + 一个转接员。
它自己不干活,身上挂着三样东西:你注册的插件(里面是函数)、你注册的 AI 服务(OpenAI / Azure / 本地模型的客户端)、你注册的过滤器。当有人要调用某个函数,它负责找到那个函数、把 kernel 自己递过去,然后闪开。
KernelFunction 才是干活的那一位,而且是唯一的一类。
这是 SK 最值得记住的一个设计判断:不管是你写的一段 Python 代码,还是一段提示词,还是远端 MCP 服务器上的一个工具,进了 SK 都被包装成同一个类型 KernelFunction。 于是"调用"只有一套流程、"元数据"只有一套格式、"过滤器"只需要拦一个地方。
一句话类比:Kernel 像操作系统的进程表 + 系统调用入口,KernelFunction 像统一的可执行文件格式——底下是脚本还是二进制无所谓,内核只认那一种格式。
1.2 用起来什么样(最小示例)
下面这段展示两件事:注册一个 Python 函数当插件、再用 invoke_prompt 直接跑一段提示词。
# 示意,非源码
from typing import Annotated
from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
class WeatherPlugin:
@kernel_function(description="查询某城市当前气温") # 标记 + 补描述
def get_temperature(
self, city: Annotated[str, "城市名,例如 'Seattle'"] # Annotated 里的字符串 = 参数描述
) -> Annotated[str, "一句话的气温描述"]:
return f"{city} 现在 21 摄氏度"
kernel = Kernel()
kernel.add_service(...) # 注册一个 chat 服务
kernel.add_plugin(WeatherPlugin(), plugin_name="weather") # 对象 → 插件 → 函数
result = await kernel.invoke(plugin_name="weather", function_name="get_temperature", city="Seattle")
answer = await kernel.invoke_prompt("用一句话点评这个天气:{{$temp}}", temp=str(result))
重点看:两次调用走的是同一个 Kernel.invoke 路径。第二次只是 SK 临时替你造了一个由提示词构成的 KernelFunction。
1.3 顶层全景:一次调用怎么流
先给一张结构图。从上往下读,箭头是控制流:
┌──────────────────── Kernel ────────────────────┐
你的代码 ──invoke──▶│ plugins 插件名 → 函数名 → KernelFunction │
│ services service_id → AI 客户端 │
│ filters 三类拦截器列表 │
│ selector 按 settings 挑服务 │
└────────────────────┬───────────────────────────┘
│ get_function 取出一个函数
▼
KernelFunction.invoke(kernel, arguments)
│ ← 统一骨架都在这里(1.5 节)
┌────────────────┴────────────────┐
▼ ▼
KernelFunctionFromMethod KernelFunctionFromPrompt
调你的 Python 方法 渲染模板 → 挑服务 → 调模型
部件与职责对照:
| 部件 | 干什么 | 在哪 |
|---|---|---|
Kernel | 容器 + 三个调用入口 | kernel.py:62 |
KernelFunction | 抽象基类,定义统一调用骨架 | functions/kernel_function.py:78 |
KernelFunctionFromMethod | 把 Python 方法变成可调用单元 | functions/kernel_function_from_method.py:21 |
KernelFunctionFromPrompt | 把提示词变成可调用单元 | functions/kernel_function_from_prompt.py:55 |
KernelPlugin | 函数的命名空间 + 各种来源的装配厂 | functions/kernel_plugin.py:36 |
KernelArguments | 入参载体(dict + 执行设置) | functions/kernel_arguments.py:18 |
FunctionResult | 出参载体(值 + 元数据 + 渲染后的提示词) | functions/function_result.py:16 |
AIServiceSelector | 决定这次用哪个 AI 服务 | services/ai_service_selector.py:17 |
1.4 Kernel:四个 mixin 拼出来的容器
Kernel 类体里几乎没有状态——它的所有字段都来自四个 mixin(混入类,只提供一组字段与方法、不能独立使用的父类):
class Kernel(KernelFilterExtension, KernelFunctionExtension, KernelServicesExtension, KernelReliabilityExtension):
kernel.py:62。四块各管一摊:
| mixin | 带进来的字段 | 关键方法 | 位置 |
|---|---|---|---|
KernelFilterExtension | function_invocation_filters / prompt_rendering_filters / auto_function_invocation_filters | add_filter、construct_call_stack | filters/kernel_filters_extension.py:30(字段 :33-35) |
KernelFunctionExtension | plugins: dict[str, KernelPlugin] | add_plugin、get_function | functions/kernel_function_extension.py:45(字段 :48) |
KernelServicesExtension | services、ai_service_selector | add_service、get_service、select_ai_service | services/kernel_services_extension.py:26(字段 :32-33) |
KernelReliabilityExtension | retry_mechanism | ——(已废弃,见 1.11) | reliability/kernel_reliability_extension.py:16 |
为什么拆成 mixin 而不是一个大类? 因为这几组能力互不依赖:过滤器不需要知道服务、插件不需要知道过滤器。拆开后每组的字段校验、pydantic 模型定义各自内聚,Kernel 本体只剩三个 invoke 入口和几个便利方法。
三个入口的区别
Kernel 对外真正的"动作"只有三类,其余都是它们的变体:
| 入口 | 输入 | 干了什么额外的事 | 位置 |
|---|---|---|---|
invoke | 一个函数(或 plugin+function 名) | 兜底捕异常并包成 KernelInvokeException | kernel.py:167-213 |
invoke_stream | 同上 | 转发流式分片;可选把分片按 choice_index 累加成完整结果 | kernel.py:104-165 |
invoke_prompt | 一段提示词字符串 | 当场造一个匿名 KernelFunctionFromPrompt,再调 invoke | kernel.py:215-255 |
invoke_prompt 的核心只有几行——没有函数名就随机生成一个(kernel.py:248-255):
function = KernelFunctionFromPrompt(
function_name=function_name or generate_random_ascii_name(),
...
)
return await self.invoke(function=function, arguments=arguments)
这解释了为什么"跑一段提示词"和"调一个工具"在 SK 里长得一模一样:提示词只是函数的一种。 流式版本 invoke_prompt_stream(kernel.py:257-324)结构完全对称。
找函数的规则在 get_function(functions/kernel_function_extension.py:276-311):给了 plugin_name 就精确定位;给 None 则遍历所有插件返回第一个同名函数,找不到抛 KernelFunctionNotFoundError。另有 get_function_from_fully_qualified_function_name(:302)按 - 拆分全限定名——分隔符是常量 DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR = "-"(const.py:9)。
1.5 KernelFunction:统一的调用骨架
这是本章最重要的一节。所有函数的 invoke 都走同一段代码,子类只实现一个抽象方法。
骨架长什么样
KernelFunction.invoke(functions/kernel_function.py:240-297)按顺序做六件事:
KernelFunction.invoke(kernel, arguments, metadata)
│
├─① arguments 为空就用 kwargs 现建一个 KernelArguments (:259-260)
├─② 建 FunctionInvocationContext(function / kernel / args) (:262)
├─③ 开 OTel span,记函数全限定名、(可选)记入参明文 (:264-269)
├─④ 计时起点 time.perf_counter() (:272)
│
├─⑤ stack = kernel.construct_call_stack( (:274-277)
│ FUNCTION_INVOCATION, inner_function=self._invoke_internal)
│ → filter_A ─▶ filter_B ─▶ _invoke_internal
│ await stack(context) 结果由子类写回 context.result (:278)
│
├─⑥ return context.result (:290)
└─ finally: 时长写进直方图 invocation_duration_histogram (:294-297)
三点值得单独拎出来:
- 结果不靠返回值传递,靠改 context。
_invoke_internal的契约是"更新context.result",而不是 return(抽象声明见functions/kernel_function.py:227-238)。这样过滤器才能在函数跑完后改结果——因为大家共享同一个 context 对象。 - 过滤器栈是运行时现搭的。
construct_call_stack(filters/kernel_filters_extension.py:108-118)把_invoke_internal放在栈底,再用partial(filter, next=stack[0])逐个往前塞,返回最外层那一个(核心只有:114-118五行) 。所以过滤器天然是洋葱模型。细节见 03。 - 可观测性是骨架自带的,不是可选装饰。 每个
KernelFunction实例都持有两个 OTel 直方图字段,靠default_factory自动创建(functions/kernel_function.py:101-106,工厂函数_create_function_duration_histogram在:62-75)。指标名是semantic_kernel.function.invocation.duration,标签为函数全限定名。
流式是对称的另一条骨架
invoke_stream(functions/kernel_function.py:308-379)与 invoke 结构一致,差别在两处:context 带 is_streaming=True(:332-334);栈底换成 _invoke_internal_stream(:346-349);拿到 context.result.value 后按是不是生成器分三种情况逐片 yield(:352-364)。计时落到另一个直方图 streaming_duration_histogram(:377-378)。
抽象点只有两个
| 抽象方法 | 契约 | 声明处 |
|---|---|---|
_invoke_internal(context) | 把结果写进 context.result | functions/kernel_function.py:227-238 |
_invoke_internal_stream(context) | 把一个(异步)生成器塞进 context.result.value | functions/kernel_function.py:299-306 |
要新增一种可调用单元,只需实现这两个方法。 官方自己就只实现了两种。
1.6 两种实现
A. KernelFunctionFromMethod —— 包一个 Python 方法
构造时先做一道门禁:方法必须带 __kernel_function__ 属性,否则直接抛 FunctionInitializationError(functions/kernel_function_from_method.py:49-50)。元数据全部从装饰器留下的属性里读,不再二次解析签名(:54-66)。
调用逻辑短得出奇(:97-116):
async def _invoke_internal(self, context: FunctionInvocationContext) -> None:
function_arguments = self.gather_function_parameters(context)
result = self.method(**function_arguments)
if isasyncgen(result):
result = [x async for x in result]
elif isawaitable(result):
result = await result
...
同步函数、协程、生成器、异步生成器四种写法统一收敛成一个值。注意第 4 行:非流式路径下调一个异步生成器函数,会把它整个耗尽收成 list。
真正有意思的是 gather_function_parameters(:152-192),它做三件事:
-
四个"魔法参数名"被内核直接注入,不从用户参数里取:
参数名 注入什么 行 kernel当前 Kernel实例:158-160serviceselect_ai_service(...)[0]选出的 AI 服务:161-163execution_settingsselect_ai_service(...)[1]:164-166arguments整个 KernelArguments:167-169这就是为什么你在插件方法签名里写
kernel: Kernel就能拿到内核——它是约定,不是依赖注入容器。 -
按元数据里的类型对象做转换(
:170-186):有type_object且不是联合类型时,调_parse_parameter(:124-150)。它优先走 pydantic 的model_validate,其次递归处理list[...],最后兜底param_type(value)或param_type(**value)。这让 LLM 传来的一坨 JSON dict 能直接变成你的 pydantic 模型。 -
缺必填就抛错(
:187-190),缺可选则只留日志、连键都不放进去——由 Python 默认值兜底。
B. KernelFunctionFromPrompt —— 包一段提示词
它的 _invoke_internal(functions/kernel_function_from_prompt.py:170-241)是一台按服务类型分派的四路开关。先渲染提示词,再看选出来的服务是哪种基类:
| 服务基类 | 调什么方法 | 输入形态 | 行 |
|---|---|---|---|
ChatCompletionClientBase | get_chat_message_contents | 渲染结果解析成 ChatHistory | :177-197 |
TextCompletionClientBase | get_text_contents | unescape 后的纯文本 | :199-211 |
TextToImageClientBase | get_image_content | 文本当图像描述 | :213-225 |
TextToAudioClientBase | get_audio_content | 文本当朗读内容 | :227-239 |
四种都落到同一个 _create_function_result(:305-326),都不匹配则抛 ValueError(:241)。
关键细节:HTML 反转义只在文本/图像/音频三路做,聊天路不做——因为聊天路走 ChatHistory.from_rendered_prompt 自己解析(:178)。这与 SK 模板默认转义变量的行为配套,细节见 02。
渲染这一步本身也套了一层过滤器(_render_prompt,:270-299):建 PromptRenderContext → 用 FilterTypes.PROMPT_RENDERING 搭栈 → 跑完再 select_ai_service。所以**「选哪个模型」是在提示词渲染之后才决定的**——过滤器有机会 在渲染阶段改写参数进而影响选服务的结果。
流式路径只支持两种服务(:250-264):chat 与 text。图像和音频没有流式实现,直接抛 FunctionExecutionException。
1.7 元数据从哪来:kernel_function 装饰器
装饰器不包装函数、不改变行为,它只往函数对象上挂属性。 看主体(functions/kernel_function_decorator.py:59-80):
setattr(func, "__kernel_function__", True)
setattr(func, "__kernel_function_description__", description or func.__doc__)
setattr(func, "__kernel_function_name__", name or getattr(func, "__name__", "unknown"))
setattr(func, "__kernel_function_streaming__", isasyncgenfunction(func) or isgeneratorfunction(func))
func_sig = signature(func, eval_str=True)
annotations = _process_signature(func_sig)
setattr(func, "__kernel_function_parameters__", annotations)
functions/kernel_function_decorator.py:13。没写 description 就吃 docstring、没写 name 就吃函数名——这个"合理默认"省掉了绝大多数样板。eval_str=True 让字符串形式的类型注解也能求值。
参数怎么被反推出来
_process_signature(:115-132)逐个参数走一遍,跳过 self,把 arg.default 当默认值,再交给 _parse_parameter(:135-193)。后者是本节的核心,规则如下:
| 情况 | 结果 | 行 |
|---|---|---|
| 有默认值 | 记 default_value,is_required=False | :140-142 |
| 无默认值 | is_required=True | :143-144 |
| 无注解 | type_="Any" | :145-147 |
Annotated[...] 里第一个 str | 当作参数描述 | :151-152 |
Annotated[...] 里的 dict | 整个并入参数元数据 | :153-157 |
联合类型里出现 None | is_required=False,默认值补 None | :165-169 |
list / dict 泛型 | 拼成 list[str] 这样的字符串 | :173-174 |
| 其他联合 | 拼成 int, str 这样的逗号串 | :175-176 |
dict 元数据这条最容易被忽略,但很实用。 写 Annotated[str, "描述", {"include_in_function_choices": False}],这个参数就不会出现在给 LLM / MCP 看的函数声明里;同时 _parse_parameter 末尾会把它的 is_required 强制置为 False(:191-192),免得声明里没有、却又被当成必填。
返回值走同一套(:73-79):从 func_sig.return_annotation 解析出 __kernel_function_return_type__ / _description__ / _required__ 三个属性。
两层元数据模型
装饰器留下的是裸 dict,构造 KernelFunctionFromMethod 时才升级成正式模型(functions/kernel_function_from_method.py:57):
Python 签名 + Annotated
│ @kernel_function 反推
▼
__kernel_function_parameters__ (list[dict])
│ KernelParameterMetadata(**param)
▼
KernelParameterMetadata ──┐
├──▶ KernelFunctionMetadata ──▶ 喂给 LLM / MCP 的工具声明
返回值三属性 ──────────────┘
KernelParameterMetadata(functions/kernel_parameter_metadata.py:12)在 pydantic 校验阶段就顺手把 JSON Schema 算好存进schema_data(form_schema,:24-35;infer_schema,:37-62)。有type_object就用它建 schema,没有就退回按类型名字符串建。默认值会被追加进描述里(:52-59)。KernelFunctionMetadata(functions/kernel_function_metadata.py:13)是函数的对外名片:名字、插件名、描述、参数表、返回参数、is_prompt。fully_qualified_name(:25-36)拼成plugin-function。
这份元数据是全书的枢纽:自动函数调用靠它生成工具声明(见 03),MCP 导出也靠它(见下一节)。
1.8 插件:函数的命名空间与来源
KernelPlugin(functions/kernel_plugin.py:36)本质是一个带名字的函数字典——它实现了 __getitem__ / __setitem__ / get / update / __contains__ / __iter__(:104-198),用起来跟 dict 一样。
一个反直觉但重要的行为:往插件里放函数时,函数会被复制而不是引用。 _parse_or_copy(:461-468)对已有的 KernelFunction 调 function_copy,后者浅拷贝对象但深拷贝 metadata,并改写其中的 plugin_name(functions/kernel_function.py:381-394)。所以同一个函数可以同时挂在两个插件下、各自带不同的全限定名,互不干扰。
五种来源
插件真正的价值是把五花八门的东西统一装配成 KernelFunction:
| 来源 | 类方法 | 怎么做 | 位置 |
|---|---|---|---|
| Python 对象 / 类实例 | from_object | inspect.getmembers 扫出所有带 __kernel_function__ 的成员 | :214-250(扫描 :239-247) |
| 目录 | from_directory | 一层展 开:子目录 → prompt 函数,.yaml → prompt 函数,.py → 递归 | :252-343 |
单个 .py 文件 | from_python_file | importlib 动态加载,找第一个含 kernel_function 的类并实例化 | :385-417 |
| OpenAPI 文档 | from_openapi | 交给 create_functions_from_openapi | :345-383 |
| MCP 服务器 | 见下 | 远端工具变本地方法 | connectors/mcp.py:236 |
from_directory 的三条分支值得记(:312-340):子目录走 KernelFunctionFromPrompt.from_directory(要求 skprompt.txt + config.json,functions/kernel_function_from_prompt.py:359-416);.yaml/.yml 走 from_yaml(:334-357);.py 走 from_python_file。任何一项失败只打 warning 不中断,但全部为空则抛 PluginInitializationError(:341-342)。
挂到内核上用 add_plugin(functions/kernel_function_extension.py:64-124)。它有个小钩子:若插件对象实现了 added_to_kernel 方法,注册后会被回调、拿到 kernel 引用(:112-113)。
MCP:两个方向都通
入向——远端工具变本地插件。 MCPPluginBase(connectors/mcp.py:236)连上服务器后调 load_tools(:540-553):
for tool in tool_list.tools if tool_list else []:
local_name = _normalize_mcp_name(tool.name)
func = kernel_function(name=local_name, description=tool.description)(partial(self.call_tool, tool.name))
func.__kernel_function_parameters__ = _get_parameter_dicts_from_mcp_tool(tool)
setattr(self, local_name, func)
这四行是本章最巧的一段。它绕开了装饰器的签名反推:partial(self.call_tool, tool.name) 根本没有真实签名可解析,所以直接把 MCP 声明里的参数表手工塞进 __kernel_function_parameters__。塞完 setattr 到插件实例上,后面 from_object 的 getmembers 扫描就能像扫普通方法一样扫到它。load_prompts(:524-538)同理,包的是 get_prompt。
名字冲突有防护:_normalize_mcp_name 把非法字符换成 -(:227-229),_has_mcp_function_name_conflict(:511-522)检查规范化后的名字是否撞上插件自身的属性,撞了就跳过并告警。具体传输由子类实现,如 MCPStdioPlugin(:605)。
出向——整个 Kernel 变成一台 MCP 服务器。 Kernel.as_mcp_server(kernel.py:579-625)转发给 create_mcp_server_from_kernel(connectors/mcp.py:1034)。它取 kernel.get_full_list_of_function_metadata(),剔除排除项,把每个函数翻译成 types.Tool(:1031-1050):
inputSchema={
"type": "object",
"properties": {param.name: param.schema_data for param in func.parameters
if param.name and param.schema_data and param.include_in_function_choices},
"required": [...],
}
这里正好收口 1.7 节埋的线:schema_data 是 KernelParameterMetadata 在校验阶段就算好的,include_in_function_choices 就是那个能被 Annotated 里的 dict 关掉的开关。同一份元数据,进来时从 MCP 声明反推、出去时生成 MCP 声明——闭环。
1.9 参数与结果的载体
KernelArguments(functions/kernel_arguments.py:18)= 一个 dict + 一个 execution_settings 字段。
它直接继承 dict,所以传参就是传键值对。特别的只有 execution_settings:传单个设置、列表或字典都会被归一成 {service_id: settings},没有 service_id 的落到常量 DEFAULT_SERVICE_NAME(:43-52;常量在 const.py:6,值是 "default")。
两个易踩的细节:
__bool__被重写了(:54-58):参数为空但设了 execution_settings,它仍然为真。别用if not arguments判断"有没有参数"。|合并会同时合并 execution_settings(:60-95),右侧优先。
FunctionResult(functions/function_result.py:16)= 值 + 出处 + 附加信息。 四个字段:
| 字段 | 装什么 |
|---|---|
function | 产出它的 KernelFunctionMetadata |
value | 真正的返回值(方法的返回值,或一个 ChatMessageContent 列表) |
rendered_prompt | 提示词函数才有:实际发出去的那段文本 |
metadata | 入参、用到的参数、chat history、原始 metadata 等 |
__str__(:38-56)做了不少体贴:值是列表且首项是 KernelContent 就取首项,否则逗号拼接;是 dict 则取最后一个值(源码里带 TODO 注释说明这是为了让一个集成测试通过)。get_inner_content(:58-68)取出模型原始响应对象——需要读 token 用量、finish_reason 时从这里拿。
1.10 服务选择:先到先得的三级阶梯
问题: 内核上注册了三个模型,这次调用该用哪个?
答案在 AIServiceSelector.select_ai_service(services/ai_service_selector.py:24-69),规则是"合并候选清单,然后先到先得"。
第一步,按顺序合并出一份候选 {service_id: settings}:
| 优先级 | 来源 | 行 |
|---|---|---|
| 1 | arguments.execution_settings —— 调用时传的 | :51 |
| 2 | function.prompt_execution_settings 中 arguments 里没有的那些 id | :52-55 |
| 3 | 两边都空 → 造一个 {"default": PromptExecutionSettings()} | :56-59 |
第二步,按这份字典的顺序逐个试,第一个能在内核里找到且类型匹配的服务就赢(:60-66):
候选 id 依次尝试 ──▶ kernel.get_service(service_id, type=type_)
│
找到 ──────────▶ 检查 settings 是不是该服务要求的设置类
│ 是 → 直接返回 (service, settings)
│ 否 → from_prompt_execution_settings 转 换后返回
└── KernelServiceNotFoundError → 试下一个
全部试完仍无 ──────▶ 抛 KernelServiceNotFoundError("No service found.")
"default" 这个 id 有特殊语义。 get_service(services/kernel_services_extension.py:68-109)里:没给 service_id 就当成 "default";而当 id 恰好是 "default" 却查不到时,不报错,直接返回该类型下的第一个服务(:100-105)。这就是"什么都不配也能跑起来"的原因。
不带 type_ 参数时,默认接受四类客户端:text / chat / text-to-audio / text-to-image(services/ai_service_selector.py:43-49)。而流式提示词函数会显式把范围收窄到 text 与 chat 两类(functions/kernel_function_from_prompt.py:292)。
要换策略,继承 AIServiceSelector 重写这一个方法即可,构造 Kernel 时传进去(kernel.py:83、:100-101)。
1.11 边界与坑
KernelReliabilityExtension是个空壳。retry_mechanism字段被显式标了deprecated("...doesn't have any effect on the kernel.")(reliability/kernel_reliability_extension.py:19-23)。四个 mixin 里这一个目前不提供任何能力,重试要靠各 connector 自己。Kernel.invoke会吞掉取消 异常。OperationCancelledException被捕获后只打 info 日志并 返回None(kernel.py:203-205);其余异常一律包成KernelInvokeException(:206-213)。所以拿到None时要分清是取消了还是函数本来就没结果。invoke_stream的 docstring 与签名不符。 文档说"if a list of functions is provided ... only the last one is streamed"(kernel.py:116-117),但签名只接受单个function(:106)。这是历史遗留的陈旧注释,别照着用。clone()不是深拷贝。 插件被重建、metadata 深拷,但底层 callable 与 service 客户端是共享的(kernel.py:542-576);源码注释直言这是为了避开 MCP 会话这类不可 pickle 的对象。- 非流式调用生成器函数会被一次性耗尽。 见
functions/kernel_function_from_method.py:104-109。 - 提示词函数不支持图像/音频流式。
_invoke_internal_stream只认 chat 与 text(functions/kernel_function_from_prompt.py:250-264)。 plugin_name=None的get_function会返回"碰到的第一个"。 多个插件有同名函数时结果取决于插件注册顺序(functions/kernel_function_extension.py:290-294)。
1.12 巧妙之处(可带走的技术)
- 抽象点收窄到一个方法。 骨架(context / 过滤 器 / span / 直方图)全在基类,子类只写
_invoke_internal。加一种新的可调用单元成本极低——functions/kernel_function.py:227-238。 - "结果写进 context"而不是"return"。 让洋葱式过滤器能在函数返回后改结果,而不需要基类为每种过滤器写特判——
functions/kernel_function.py:278-290。 - 装饰器只挂属性、不包装函数。 被装饰的方法仍能被普通 Python 代码直接调用,测试无摩擦——
functions/kernel_function_decorator.py:59-80。 - 元数据在 pydantic 校验期就把 JSON Schema 算好。 生成工具声明时零成本——
functions/kernel_parameter_metadata.py:24-35。 - 给
partial手工塞参数元数据,绕过签名反推。 MCP 这类"运行期才知道签名"的来源因此也能复用同一套元数据管线——connectors/mcp.py:592-597。 - 函数入插件即复制并改写 plugin_name。 同一函数可挂多处、互不污染——
functions/kernel_plugin.py:461-468配合functions/kernel_function.py:381-394。
1.13 代码地图
| 主题 | 文件(相对 python/semantic_kernel/) | 符号 |
|---|---|---|
| 内核本体与三个入口 | kernel.py | Kernel、invoke、invoke_stream、invoke_prompt |
| 插件容器 mixin | functions/kernel_function_extension.py | KernelFunctionExtension、add_plugin、get_function |
| 服务容器 mixin | services/kernel_services_extension.py | KernelServicesExtension、get_service、select_ai_service |
| 过滤器容器 mixin | filters/kernel_filters_extension.py | KernelFilterExtension、construct_call_stack |
| 可靠性 mixin(已废弃) | reliability/kernel_reliability_extension.py | KernelReliabilityExtension、retry_mechanism |
| 调用骨架 | functions/kernel_function.py | KernelFunction、invoke、invoke_stream、_invoke_internal |
| 方法实现 | functions/kernel_function_from_method.py | KernelFunctionFromMethod、gather_function_parameters、_parse_parameter |
| 提示词实现 | functions/kernel_function_from_prompt.py | KernelFunctionFromPrompt、_invoke_internal、_render_prompt |
| 元数据反推 | functions/kernel_function_decorator.py | kernel_function、_process_signature、_parse_parameter |
| 函数名片 | functions/kernel_function_metadata.py | KernelFunctionMetadata、fully_qualified_name |
| 参数名片与 schema | functions/kernel_parameter_metadata.py | KernelParameterMetadata、infer_schema |
| 插件与装配 | functions/kernel_plugin.py | KernelPlugin、from_object、from_directory、from_openapi、from_python_file |
| 入参载体 | functions/kernel_arguments.py | KernelArguments |
| 出参载体 | functions/function_result.py | FunctionResult、get_inner_content |
| 服务选择 | services/ai_service_selector.py | AIServiceSelector、select_ai_service |
| MCP 双向桥 | connectors/mcp.py | MCPPluginBase、load_tools、MCPStdioPlugin、create_mcp_server_from_kernel |
| 关键常量 | const.py | DEFAULT_SERVICE_NAME、DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR |
下一步: 上面反复提到"渲染提示词",但没讲模板语法和内容模型——那是 第 02 章。而"过滤器栈"和"模型自己决定调哪个函数"的循环,在 第 03 章。