跳到主要内容

数据截至 (上游 commit 5e1f1fb87d9a)

第 01 章 · 内核与可调用单元:Kernel、KernelFunction、插件

本章讲什么: SK 的最小心智模型只有两个名词——Kernel 是容器 + 调度器,KernelFunction 是唯一的可调用单元。本章把这两个词彻底讲透:Kernel 由哪四块拼成、一次调用的骨架长什么样、函数的元数据是怎么从 Python 签名反推出来的、插件如何充当函数的命名空间与来源。

不讲: 提示词模板语法与内容模型(见 02)、自动函数调用循环与三类过滤器的细节(见 03)。

引用路径约定: 本章所有 file:line 均相对克隆里的 python/semantic_kernel/ 目录。例如 kernel.py:62python/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带进来的字段关键方法位置
KernelFilterExtensionfunction_invocation_filters / prompt_rendering_filters / auto_function_invocation_filtersadd_filterconstruct_call_stackfilters/kernel_filters_extension.py:30(字段 :33-35)
KernelFunctionExtensionplugins: dict[str, KernelPlugin]add_pluginget_functionfunctions/kernel_function_extension.py:45(字段 :48)
KernelServicesExtensionservicesai_service_selectoradd_serviceget_serviceselect_ai_serviceservices/kernel_services_extension.py:26(字段 :32-33)
KernelReliabilityExtensionretry_mechanism——(已废弃,见 1.11)reliability/kernel_reliability_extension.py:16

为什么拆成 mixin 而不是一个大类? 因为这几组能力互不依赖:过滤器不需要知道服务、插件不需要知道过滤器。拆开后每组的字段校验、pydantic 模型定义各自内聚,Kernel 本体只剩三个 invoke 入口和几个便利方法。

三个入口的区别

Kernel 对外真正的"动作"只有三类,其余都是它们的变体:

入口输入干了什么额外的事位置
invoke一个函数(或 plugin+function 名)兜底捕异常并包成 KernelInvokeExceptionkernel.py:167-213
invoke_stream同上转发流式分片;可选把分片按 choice_index 累加成完整结果kernel.py:104-165
invoke_prompt一段提示词字符串当场造一个匿名 KernelFunctionFromPrompt,再调 invokekernel.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.resultfunctions/kernel_function.py:227-238
_invoke_internal_stream(context)把一个(异步)生成器塞进 context.result.valuefunctions/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),它做三件事:

  1. 四个"魔法参数名"被内核直接注入,不从用户参数里取:

    参数名注入什么
    kernel当前 Kernel 实例:158-160
    serviceselect_ai_service(...)[0] 选出的 AI 服务:161-163
    execution_settingsselect_ai_service(...)[1]:164-166
    arguments整个 KernelArguments:167-169

    这就是为什么你在插件方法签名里写 kernel: Kernel 就能拿到内核——它是约定,不是依赖注入容器。

  2. 按元数据里的类型对象做转换(:170-186):有 type_object 且不是联合类型时,调 _parse_parameter(:124-150)。它优先走 pydantic 的 model_validate,其次递归处理 list[...],最后兜底 param_type(value)param_type(**value)。这让 LLM 传来的一坨 JSON dict 能直接变成你的 pydantic 模型。

  3. 缺必填就抛错(:187-190),缺可选则只留日志、连键都不放进去——由 Python 默认值兜底。

B. KernelFunctionFromPrompt —— 包一段提示词

它的 _invoke_internal(functions/kernel_function_from_prompt.py:170-241)是一台按服务类型分派的四路开关。先渲染提示词,再看选出来的服务是哪种基类:

服务基类调什么方法输入形态
ChatCompletionClientBaseget_chat_message_contents渲染结果解析成 ChatHistory:177-197
TextCompletionClientBaseget_text_contentsunescape 后的纯文本:199-211
TextToImageClientBaseget_image_content文本当图像描述:213-225
TextToAudioClientBaseget_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
联合类型里出现 Noneis_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_promptfully_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)对已有的 KernelFunctionfunction_copy,后者浅拷贝对象但深拷贝 metadata,并改写其中的 plugin_name(functions/kernel_function.py:381-394)。所以同一个函数可以同时挂在两个插件下、各自带不同的全限定名,互不干扰。

五种来源

插件真正的价值是把五花八门的东西统一装配成 KernelFunction:

来源类方法怎么做位置
Python 对象 / 类实例from_objectinspect.getmembers 扫出所有带 __kernel_function__ 的成员:214-250(扫描 :239-247)
目录from_directory一层展开:子目录 → prompt 函数,.yaml → prompt 函数,.py → 递归:252-343
单个 .py 文件from_python_fileimportlib 动态加载,找第一个含 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/.ymlfrom_yaml(:334-357);.pyfrom_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_objectgetmembers 扫描就能像扫普通方法一样扫到它。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_dataKernelParameterMetadata 在校验阶段就算好的,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}:

优先级来源
1arguments.execution_settings —— 调用时传的:51
2function.prompt_execution_settingsarguments 里没有的那些 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=Noneget_function 会返回"碰到的第一个"。 多个插件有同名函数时结果取决于插件注册顺序(functions/kernel_function_extension.py:290-294)。

1.12 巧妙之处(可带走的技术)

  1. 抽象点收窄到一个方法。 骨架(context / 过滤器 / span / 直方图)全在基类,子类只写 _invoke_internal。加一种新的可调用单元成本极低——functions/kernel_function.py:227-238
  2. "结果写进 context"而不是"return"。 让洋葱式过滤器能在函数返回后改结果,而不需要基类为每种过滤器写特判——functions/kernel_function.py:278-290
  3. 装饰器只挂属性、不包装函数。 被装饰的方法仍能被普通 Python 代码直接调用,测试无摩擦——functions/kernel_function_decorator.py:59-80
  4. 元数据在 pydantic 校验期就把 JSON Schema 算好。 生成工具声明时零成本——functions/kernel_parameter_metadata.py:24-35
  5. partial 手工塞参数元数据,绕过签名反推。 MCP 这类"运行期才知道签名"的来源因此也能复用同一套元数据管线——connectors/mcp.py:592-597
  6. 函数入插件即复制并改写 plugin_name。 同一函数可挂多处、互不污染——functions/kernel_plugin.py:461-468 配合 functions/kernel_function.py:381-394

1.13 代码地图

主题文件(相对 python/semantic_kernel/)符号
内核本体与三个入口kernel.pyKernelinvokeinvoke_streaminvoke_prompt
插件容器 mixinfunctions/kernel_function_extension.pyKernelFunctionExtensionadd_pluginget_function
服务容器 mixinservices/kernel_services_extension.pyKernelServicesExtensionget_serviceselect_ai_service
过滤器容器 mixinfilters/kernel_filters_extension.pyKernelFilterExtensionconstruct_call_stack
可靠性 mixin(已废弃)reliability/kernel_reliability_extension.pyKernelReliabilityExtensionretry_mechanism
调用骨架functions/kernel_function.pyKernelFunctioninvokeinvoke_stream_invoke_internal
方法实现functions/kernel_function_from_method.pyKernelFunctionFromMethodgather_function_parameters_parse_parameter
提示词实现functions/kernel_function_from_prompt.pyKernelFunctionFromPrompt_invoke_internal_render_prompt
元数据反推functions/kernel_function_decorator.pykernel_function_process_signature_parse_parameter
函数名片functions/kernel_function_metadata.pyKernelFunctionMetadatafully_qualified_name
参数名片与 schemafunctions/kernel_parameter_metadata.pyKernelParameterMetadatainfer_schema
插件与装配functions/kernel_plugin.pyKernelPluginfrom_objectfrom_directoryfrom_openapifrom_python_file
入参载体functions/kernel_arguments.pyKernelArguments
出参载体functions/function_result.pyFunctionResultget_inner_content
服务选择services/ai_service_selector.pyAIServiceSelectorselect_ai_service
MCP 双向桥connectors/mcp.pyMCPPluginBaseload_toolsMCPStdioPlugincreate_mcp_server_from_kernel
关键常量const.pyDEFAULT_SERVICE_NAMEDEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR

下一步: 上面反复提到"渲染提示词",但没讲模板语法和内容模型——那是 第 02 章。而"过滤器栈"和"模型自己决定调哪个函数"的循环,在 第 03 章