跳到主要内容

semantic-kernel — 本课题摘录

读了哪几篇: 03-function-calling-and-filters(自动函数调用循环与三类过滤器)。 其余五篇(内核与函数、提示词与内容模型、Agent 层、多 agent 编排、流程与数据)本轮没读。

这一家在本课题里的位置:它把"工具调用出错了怎么办"拆得最细——五道关卡,每一道的出口都是给模型的一句人话。

它对本课题回答了什么

它先把"从模型说要调工具、到结果回到对话"之间的活拆成五件

序号要干的事
告诉模型有哪些工具可用
从回复里挑出调用请求
找到并执行真函数(名字对不对、参数齐不齐、执行会不会炸)
把结果变成一条"工具"角色的消息追加进历史
再问模型一次

(依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— 把「模型说要调工具」到「结果回到对话」之间拆成五件必须有人干的事:暴露工具、接住调用请求、找到并执行真函数、结果写回历史、再问模型)

这五件事就是本课题四个决定的一个更细的切法,值得直接拿去当讲义的分节。

决定四:轮数用光不是报错,是"摘掉工具再问最后一次"

它的循环骨架里有一处别家没有的收尾:

for 每一轮(默认 5 轮上限):
问模型
没有工具调用 → 收工
有 → 并行执行,结果进历史
有人喊停 → 直接交工具结果
否则(轮数用光):
把工具从请求里摘掉,再问模型一次 ← 这一步

(依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— 自动函数调用是「最多跑 N 轮、每轮并行执行工具、任何错误都转成文本喂回模型」的 for 循环,轮数用光后把 settings.tools 置空再问模型一次)

这比"到上限抛错"友好得多:摘掉工具之后模型只能用文字回答,于是一定会给出一个人话结论,而不是把用户扔在半路。 cline 是抛错兜底,crewai 是强制逼出最终答案,这里是摘掉工具再问一次——三种做法里这一种最不伤用户。

决定三:单次工具调用是一条"几乎不抛异常"的五关流水线

它把设计原则写成一句话:模型犯的任何错,都不该炸掉程序,而应该变成一段文字告诉模型"你错在哪、重来"。

关卡出口(给模型的话)
① 名字为空 / 不在白名单 / 函数不存在"该工具不在提供的工具列表里,请核对名字"
② 参数缺失 / 有多余参数"缺少 [x];收到多余 [y];请对照签名修正"
③ 参数不是合法 JSON"参数格式错误,必须是 JSON,请重试"
④ 必填个数再兜一次"需要 N 个参数,只收到 M 个"
⑤ 真函数执行炸了"调用 X 时出错:<异常信息>"

(依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— invoke_function_call 是五道关卡的流水线,出口分别是「工具不在列表」「参数缺失/多余」「参数不是合法 JSON」「必填个数不足」「执行炸了」,每一道都转成给模型的文本)

五道关卡的出口形态完全一样:把错误文本包成一条工具结果、追加进历史、返回"这次没喊停"——循环照常进下一轮,模型看到那段错误文本自己纠正。

参数校验的错误消息里刻意包含排序后的参数名,让模型能精确定位。 (依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— 参数校验的错误消息刻意包含 sorted 后的参数名,让模型能精确定位缺了哪个/多了哪个)

参数解析还有一道容错: JSON 解析失败时,把非转义的单引号替换成双引号再试一次——专治模型输出 Python 风格的 {'a': 1}。两次都失败才抛。 (依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— parse_arguments 在 JSON 解析失败时把非转义单引号替换成双引号再试一次,专治模型输出 Python 风格的 {'a': 1})

这条很小,但是从真实失败里长出来的经验,值得直接抄进配方。

决定二:白名单要查两遍

已经只把白名单里的工具发给模型了,执行前还要再查一遍名字在不在白名单里。

理由写得很直白:模型可以幻觉出一个没给过的名字,而系统上真的挂着那个函数。 少了这道校验,一个"只准用计算插件"的会话就可能被模型骗着调到"发邮件"。 (依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— 执行前对 function_call.name 再做一次白名单校验,理由是模型可能幻觉出没给过的名字而 kernel 上真的挂着那个函数)

这是一条安全边界,不是防呆。 而且它注意到:没传行为配置时不做校验,但会打一条日志明确提示"本次未做白名单校验"——把"没设防"这件事说出来,而不是默默放行。

决定三补充:流式版有四处不一样

方面非流式流式
怎么拿到调用一次返回直接取边收边转发,收完把碎片加起来
工具结果怎么给调用方只在喊停时返回每轮都给一次合并后的结果消息
达上限后摘掉工具再问一次没有这一步,循环跑完直接结束

(依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— 流式循环与非流式有四处差异,其中流式没有「达上限后摘掉工具再问一次」的分支,循环跑完直接结束)

这是一处真实的行为不一致,而且是"流式反而更差"的那种。 记在配方里:流式和非流式要跑同一条收尾路径。

它的做法(可以抄的部分)

整个自动调用循环被一个观测跨度罩着,覆盖全部轮次,不是每轮一个。 跨度上挂一个属性:所有可用函数的全限定名。 (依据:前沿库 · Semantic Kernel · 主线:自动函数调用循环与三类过滤器 —— 整个自动函数调用循环被一个名为 AutoFunctionInvocationLoop 的 span 罩着,覆盖全部轮次而非每轮一个)

过滤器是洋葱,注册用头插——新加的排在最前面。存的是"标识 + 函数"的元组,那个标识是给"移除过滤器"用的句柄。

它没回答什么

  • 历史怎么压——这一章不管长对话。
  • 循环状态怎么存——没有可序列化状态这个概念。
  • 工具清单怎么每轮变——有"这次给模型看哪几个"的机制,但那是会话级配置,不是每轮重算。

坑与代价

  • 流式路径少了收尾那一步(见上)。抄的时候要补上。
  • "错误即文本"的代价是循环可能空转。 模型如果反复犯同一个错,五关会反复给它同一句提示,直到轮数用光。它没有连错熔断——那是 cline 有而它没有的。