文章阅读目录大纲
https://github.com/xieguigang/LLMs
已知,现在我们可以成功的和正在运行的大语言模型服务勾搭了,现在能够让大语言模型为我们做些什么。很遗憾的是,由于大语言模型本质上只是一个数学模型,其作用只是针对我们的输入找出最佳的字符输出组合。如果我们没有额外的针对大语言模型进行拓展,我们所勾搭上的大语言模型充其量也只是一个聊天机器人,他既不能帮我们发送email,也不能够帮助我们调节屋内的灯光,只能够做到分析我们输入的文本,然后输出一段最佳的文本。所以我们需要通过针对大语言模型添加额外的拓展来帮助我们实现各种功能。
又已知,大语言模型的本质就是进行文本的结构化分析,那么假如我们的输入信息中包含有某些工具函数的描述信息,而且大语言模型能够正确的分析出我们的输入文本和输入信息中所包含的工具函数之间的对应关系,那么大语言模型的输出就可以专门定向的变换为一种针对输入信息所对应的函数调用的结构化文本信息输出。当运行大语言模型的基础服务捕捉到这种结构化文本(例如json)输出后就可以通过这种结构化文本信息的内容解析结果来调用对应的外部工具,这样子我们就可以让大语言模型来帮助我们完成特定的任务了。这种特性就是大语言模型的Function Calling功能。
下面的流程图向我们展示了LLMs通过Function Calling能力调用外部工具的整个过程:
sequenceDiagram
participant Human
participant Ollama(DeepSeek)
participant Tools Server
Human->>Ollama(DeepSeek): Hello, What's the price of gold today
loop LLMs
Ollama(DeepSeek)->>Ollama(DeepSeek): LLMs thinking
end
Ollama(DeepSeek)-->>Tools Server: tolls_call: gold_price(today)
loop tool
Tools Server->>Tools Server: Query Internet, get gold price
end
Tools Server-->>Ollama(DeepSeek): 1000 RMB/g
loop LLMs
Ollama(DeepSeek)->>Ollama(DeepSeek): LLMs thinking
end
Ollama(DeepSeek)-->>Human: 1000 RMB/g!
下面,我们就从上面的整个流程来看LLMs是如何实现上面的整个过程的。
工具信息定义
在前面的文章《从头创建一个DeepSeek客户端》的请求消息的数据结构的基础上,我们在这里再增加一个工具信息的列表,在这个工具信息列表中,包括了工具的名称,工具的描述信息,以及工具的参数信息。对应的新增加的数据结构如下所示:
Public Class RequestBody
' ...
Public Property tools As FunctionTool()
' ...
End Class
' definition of the function tools for LLMs reading
Public Class FunctionModel
Public Property name As String
Public Property description As String
Public Property parameters As FunctionParameters
End Class
Public Class FunctionParameters
Public Property type As String = "object"
Public Property properties As Dictionary(Of String, ParameterProperties)
Public Property required As String()
End Class
Public Class ParameterProperties
Public Property name As String
Public Property description As String
Public Property type As String = "string"
End Class
Public Class FunctionTool
Public Property type As String = "function"
Public Property [function] As FunctionModel
<MethodImpl(MethodImplOptions.AggressiveInlining)>
Public Shared Function CreateToolSet(ParamArray f As FunctionModel()) As FunctionTool()
Return (From fi As FunctionModel
In f
Select New FunctionTool With {
.[function] = fi
}).ToArray
End Function
End Class
例如,我们要定义一个没有什么卵用的查询时间的工具,可以创建如下的对象实例,然后设置到发送给大语言模型的消息数据之中:
Dim tool_time As New FunctionModel With {
.description = "get time of now",
.name = "get_time",
.parameters = New FunctionParameters With {
.required = {"loc"},
.properties = New Dictionary(Of String, ParameterProperties) From {
{
"loc", New ParameterProperties With {
.name = "loc",
.description = "the location name for get current local time"
}
}
}
}
}
Dim req As New RequestBody With {
.messages = chat_message,
.model = model,
.stream = True,
.temperature = 0.1,
.tools = {tool_time}
}
就这样子就可以了,非常简单吧。当LLMs接收到了我们的聊天消息,例如我们问:“what is the time of beijing city now?”。LLMs在处理的时候发现我们的内容和
这个工具的关联性还非常高的。于是就基于了解到的函数工具描述信息,生成了用于外部工具调用的结构化的(json)文本内容输出,包含有函数调用的消息就会被传递到对应的工具服务器上,由工具服务器执行对应的代码,得到结果,再返回给LLMs进行结果整合为消息文本输出,返回给用户。get_time
处理工具调用消息
LLMs返回给我们的工具调用信息的数据结构如下所示:
Public Class ToolCall
Public Property id As String
Public Property [function] As FunctionCall
End Class
''' <summary>
''' the function invoke parameters
''' </summary>
Public Class FunctionCall
Public Property name As String
Public Property arguments As Dictionary(Of String, String)
End Class
在上面的工具调用消息数据结构中,我们可以清楚的看见有需要进行调用的工具名称,以及参数列表。当我们拿到这样子的调用信息后,就可以基于一定的规则找到需要执行的运行时中的函数来完成功能的实现。对于.NET平台上,我们一般是使用自定义属性加反射操作来解析相关的名称绑定结果。在.NET平台上对于这样子的一个根据调用信息来进行运行时解析和调用的方法,可以稍微参考《【Darwinism】Linux平台上的VisualBasic高性能并行计算应用的开发》中所提到的的反射代码方法来进行实现。
Dim resp As String = RequestMessage(client, url, content).GetAwaiter.GetResult
Dim jsonl As String() = resp.LineTokens
Dim msg As New StringBuilder
For Each stream As String In jsonl
Dim result = stream.LoadJSON(Of ResponseBody)
Dim deepseek_think = result.message.content
Call ai_memory.Add(result.message)
Call ai_log.WriteLine(stream)
If deepseek_think = "" AndAlso Not result.message.tool_calls.IsNullOrEmpty Then
' is function calls
' ...
End If
Call msg.Append(deepseek_think)
Next
在上面的代码中,我们展示了如何通过流式的方式来处理LLMs返回来的消息。当我们处理jsonl数据中的某一行消息的时候,发现消息文本是空字符串,但是tool_calls中的信息不是空的。那么我们这个时候就可以判定LLMs发起了一次远程工具调用。这个时候我们就要处理这个工具调用。下面的代码是属于上面的For循环中的代码,展示了如何进行工具调用,然后返回结果数据给LLMs:
Dim tool_call As ToolCall = result.message.tool_calls(0)
Dim invoke As FunctionCall = tool_call.function
Dim fval As String = If(ai_caller.CheckFunction(invoke.name), ai_caller.Call(invoke), execExternal(invoke))
Dim [next] As New History With {
.content = fval,
.role = "tool",
.tool_call_id = tool_call.id
}
Dim messages As New List(Of History)(req.messages)
Call messages.Add(result.message)
Call messages.Add([next])
req = New RequestBody(req)
req.messages = messages.ToArray
Return Chat(req)
在上面的执行过程,我们在拿到了工具调用信息后,通过ai_caller帮助函数来执行对应的外部函数,得到了外部工具的执行结果
字符串。接着我们就需要将工具返回来的字符串结果创建消息结果,合并到历史记录中重新发送给LLMs,然后LLMs对结果进行汇总输出可以被人阅读的文本内容给用户。在上面所创建的工具执行结果的消息中,消息来源的角色应该被设置为“tool”,然后发送回LLMs。fval
实现上面的工具调用的完整过程的代码,大家可以阅读在Github代码库上的代码文件:Ollama.vb
- 基于R#语言的LLMs大语言模型工具开发 - 2025年5月23日
- 为大语言模型运行添加工具调用 - 2025年5月23日
- 从头创建一个DeepSeek客户端 - 2025年5月18日
One response
[…] 《为大语言模型运行添加工具调用》 […]