写点什么

大模型应用开发初探 : 通用函数调用 Planner

  • 2024-11-15
    福建
  • 本文字数:4043 字

    阅读完需:约 13 分钟

UniversalFunctionCaller


这个项目是一个封装了大模型对话的入口,有点类似我们在 ASP.NET 中写的 Filter,在处理某个真正的请求时,给其设置一些横切面,例如 PreHandle,PostHandle 之类的方法供用户做自定义处理,最终完成所谓的 AoP(面向横切面编程)的效果。这个项目做的事儿其实也就是封装了横切面,在真正将 prompt 发给 LLM 前,它会读取一些自定义的有点类似于预训练的 prompt 来对用户的 prompt 进行“增强“。例如,下面这个方法 GetAskFromHistory 就会来 设定一个函数调用的背景 以及 给出一些预置的训练提示词供大模型理解,妥妥的一个手动增强版提示词工程:


public class UniversalFunctionCaller{    ......
public async Task<string> RunAsync(ChatHistory askHistory) { var ask = await GetAskFromHistory(askHistory); return await RunAsync(ask); }
private async Task<string> GetAskFromHistory(ChatHistory askHistory) { var sb = new StringBuilder(); var userAndAssistantMessages = askHistory.Where(h => h.Role == AuthorRole.Assistant || h.Role == AuthorRole.User); foreach (var message in userAndAssistantMessages) sb.AppendLine($"{message.Role.ToString()}: {message.Content}");
var extractAskFromHistoryPrompt = $@"阅读这段用户与助手之间的对话。 总结用户在最后一句话中希望助手做什么 ##对话开始## {sb.ToString()} ##对话结束##";
var extractAskResult = await _chatCompletion.GetChatMessageContentAsync(extractAskFromHistoryPrompt); var ask = extractAskResult.Content; return ask; }  ......}
复制代码


然后,它会初始化一个 ChatHistory,提供一些示范性的对话,让大模型知道是否该进行函数调用 以及 如何调用:


private ChatHistory InitializeChatHistory(string ask){    var history = new ChatHistory();    history.Add(new ChatMessageContent(AuthorRole.User, "New task: 启动飞船"));    history.Add(new ChatMessageContent(AuthorRole.Assistant, "GetMySpaceshipName()"));    history.Add(new ChatMessageContent(AuthorRole.User, "长征七号"));    history.Add(new ChatMessageContent(AuthorRole.Assistant, "StartSpaceship(ship_name: \"长征七号\")"));    history.Add(new ChatMessageContent(AuthorRole.User, "飞船启动"));    history.Add(new ChatMessageContent(AuthorRole.Assistant, "Finished(finalmessage: \"'长征七号'飞船启动 \")"));return history;}
复制代码


而示范用的函数则将其封装到了一个预置的 Plugin,我们暂且叫它 PreTrainingPlugin,它是一个 internal 访问的 class,只用于对 prompt 进行增强即给出示例:


internal class PreTrainingPlugin{    [KernelFunction, Description("当工作流程完成,没有更多的函数需要调用时,调用这个函数")]    public string Finished([Description("总结已完成的工作和结果,尽量简洁明了。")] string finalmessage)    {        return string.Empty;        //no actual implementation, for internal routing only    }
[KernelFunction, Description("获取用户飞船的名称")] public string GetMySpaceshipName() { return "长征七号"; }
[KernelFunction, Description("启动飞船")] public void StartSpaceship([Description("启动的飞船的名字")] string ship_name) { //no actual implementation, for internal routing only }}
复制代码


同时,它会将你定义的 Functions 总结为一个 string 列表,然后作为可用的 Function list 放到 prompt 中告诉大模型:



然后,就开始根据用户的 prompt 进行函数调用了,直到它认为不会再需要函数调用时就结束,这个方法的全部代码如下所示:


public async Task<string> RunAsync(string task){    // Initialize plugins    var plugins = _kernel.Plugins;    var internalPlugin = _kernel.Plugins.AddFromType<PreTrainingPlugin>();
// Convert plugins to text var pluginsAsText = GetTemplatesAsTextPrompt3000(plugins);
// Initialize function call and chat history var nextFunctionCall = new FunctionCall { Name = ConfigConstants.FunctionCallStatus.Start }; var chatHistory = InitializeChatHistory(task);
// Add new task to chat history chatHistory.Add(new ChatMessageContent(AuthorRole.User, $"New task: {task}"));
// Process function calls for (int iteration = 0; iteration < 10 && nextFunctionCall.Name != ConfigConstants.FunctionCallStatus.Finished; iteration++) { nextFunctionCall = await GetNextFunctionCallAsync(chatHistory, pluginsAsText); if (nextFunctionCall == null) throw new Exception("The LLM is not compatible with this approach!");
// Add function call to chat history var nextFunctionCallText = GetCallAsTextPrompt3000(nextFunctionCall); chatHistory.AddAssistantMessage(nextFunctionCallText);
// Invoke plugin and add response to chat history var pluginResponse = await InvokePluginAsync(nextFunctionCall); chatHistory.AddUserMessage(pluginResponse); }
// Remove internal plugin _kernel.Plugins.Remove(internalPlugin);
// Check if task was completed successfully if (nextFunctionCall.Name == ConfigConstants.FunctionCallStatus.Finished) { var finalMessage = nextFunctionCall.Parameters[0].Value.ToString(); return finalMessage; }
throw new Exception("LLM could not finish workflow within 10 steps. Please consider increasing the number of steps!");}
复制代码


需要特别注意的是,不建议在一个 prompt 中涉及超过 10 次函数调用,这样效果不太好,处理速度也慢,验证也不太方便。


此外,在方法内部进行函数调用的分析时,自动加了一个如下所示的 SystemMessage,用于设定一些通用的规则给到大模型进行理解:


private string GetLoopSystemMessage(string pluginsAsTextPrompt3000){   var systemPrompt = $@"你是一个计算机系统。你只能使用TextPrompt3000指令,让用户调用对应的函数,而用户将作为另一个回答这些函数的计算机系统。以下是您所需实现的目标,以及用户可以使用的函数列表。您需要找出用户到达目标的下一步,并推荐一个TextPrompt3000函数调用。 您还会得到一个TextPrompt3000 Schema格式的函数列表。TextPrompt3000格式的定义如下所示:{GetTextPrompt300Explanation()}##可用函数列表开始##{pluginsAsTextPrompt3000}##可用函数列表结束##
以下规则非常重要:1) 你只能推荐一个函数及其参数,而不是多个函数2) 你可以推荐的函数只存在于可用函数列表中3) 你需要为该函数提供所有参数。不要在函数名或参数名中转义特殊字符,直接使用(如只写aaa_bbb,不要写成aaa\_bbb)4) 你推荐的历史记录与函数需要对更接近目标有重要作用5) 不要将函数相互嵌套。 遵循列表中的函数,这不是一个数学问题。 不要使用占位符。我们只需要一个函数,下一个所需的函数。举个例子, 如果 function A() 需要在 function B()中当参数使用, 不要使用 B(A())。 而是,如果A还没有被调用, 先调用 A()。返回的结果将在下一次迭代中在B中使用。6) 不要推荐一个最近已经调用过的函数。 使用输出代替。 不要将占位符或函数作为其他函数的参数使用。7) 只写出一个函数调用,不解释原因,不提供理由。您只能写出一个函数调用!8) 当所有必需的函数都被调用,且计算机系统呈现了结果,调用Finished函数并展示结果。9) 请使用中文回答。
如果你违反了任何这些规定,那么会有一只小猫死去。"; return systemPrompt;}
复制代码


综上所示,这就是提示词工程的魔力所在!


更新后的 AI Agent 效果


这里我们快速对原来的 WorkOrder Agent 重构了一下,增加了 Use Function Planner 的 checkbox 选项,如果你勾选了它,就会使用上面介绍的 UniversalFunctionCaller 进行 prompt 的包裹和预处理,然后再发给大模型 以及 进行函数调用。


这里我修改了使用的模型和平台信息,这里我们基于 SiliconCloud 来使用一个通义千问的小参数文本生成模型 Qwen2-7B-Instruct 来试试:


{  "LLM_API_PROVIDER": "QwenAI",  "LLM_API_MODEL": "Qwen/Qwen2-7B-Instruct",  "LLM_API_BASE_URL": "https://api.siliconflow.cn",  "LLM_API_KEY": "sk-**************" // Update this value to yours}
复制代码


具体效果如下图所示:


(1)没有使用 Function Planner 的效果



(2)使用了 Function Planner 的效果



可以看到,我的需求其实包含 3 个步骤:第一步是更新工单的 Quantity,第二步是更新工单的状态,第三步是查询更新后的工单信息。而这几个步骤我们假设其实都是需要去调用 MES WorkOrderService API 才能获得的,这里我们的 Agent 理解到了要点,并分别调用了两个 function 实现了任务。


这个示例代码的结构如下所示:



我这里将 UniversalFunctionCaller 放到了解决方案中的 Shared 类库中了,源码来自 Jenscaasen 大佬的开源项目,中文翻译的 prompt 来自国内的 mingupupu 大佬的介绍。


小结


本文简单介绍了一种面向小参数量模型的通用函数调用方案,基于这个方案,我们可以在这类大模型上进行准确的函数调用,以便实现更可靠的 AI Agent。


文章转载自:EdisonZhou

原文链接:https://www.cnblogs.com/edisonchou/p/-/quick-start-with-universal-function-caller

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
大模型应用开发初探 : 通用函数调用Planner_大模型_不在线第一只蜗牛_InfoQ写作社区