
LLM只知道根据一段token预测下一个token,给它一段话,它能一个token一个token地猜出一个回答来。但是现在Agent做到的事可不仅仅是回答问题,它还能自己读代码改代码,自己设计方案写成文档,自己检查写的代码有没有问题等等。从知道到做到的关键,就是 function call(又被称为tool call,工具调用),LLM会告诉客户端,应该调用什么工具,客户端调用对应的工具,来真正做到一些事情。
一、回答日期相关问题
LLM是无状态的,更没有独立的时钟,问它时间的问题,它就完全不知道了:
(用的上一篇文的练习代码,只是调用了DeepSeek API,并没有调用工具:github.com/rengmo/ai-p...)
但是打开DeepSeek APP一问,回答得好好的:
可能是前端获取了会话时的时间信息,或者是后端调用了时间工具获取时间信息传给了模型。
这里并没有去看DeepSeek APP具体是怎么实现的。对于这次的练习来说,是在后端代码中加上了调用时间工具的逻辑,加上后就可以正常获得时间相关的答复了,这个工具是后端代码里实现的,不是LLM的能力。
- 首先定义工具的类型、名称和描述、工具的调用参数等信息:
jsx
var toolDefs = []Tool{
{
Type: "function",
Function: toolFunction{
Name: "get_current_time",
Description: "获取当前日期、时间和星期。用户问「现在几点」「今天几号」时必须调用,不要编造。",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{},
},
},
},
...
- 然后在DeepSeek的API请求里面,作为Tools参数的值传递:
jsx
body, _ := json.Marshal(apiChatRequest{Model: model, Messages: messages, Tools: toolDefs})
- 如果需要用到工具,模型会返回以下格式的数据(这个是DeepSeek API返回的,LLM本身可能只是返回某种格式的文本 ):
jsx
tool_calls: [{ name: "get_current_time" }]
- 应用程序拿到数据之后,根据name信息,调用在应用程序代码里定义好的函数:
jsx
result, _ := runTool(call.Function.Name, call.Function.Arguments)
jsx
func runTool(name, argsJSON string) (ToolResult, error) {
switch name {
case "get_current_time":
return ToolResult{Text: currentTimeText()}, nil
jsx
func currentTimeText() string {
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
loc = time.FixedZone("CST", 8*3600)
}
now := time.Now().In(loc)
weekdays := []string{"日", "一", "二", "三", "四", "五", "六"}
return fmt.Sprintf("%s 星期%s", now.Format("2006年1月2日 15:04:05"), weekdays[now.Weekday()])
}
- 拿到函数调用的结果后,把结果添加到message列表中,下一轮循环中,用包含tool调用结果的message列表,传给LLM,获取最终结果:
jsx
working = append(working, Message{
Role: "tool",
ToolCallID: call.ID,
Content: result.Text,
})
jsx
for step := 1; step <= 8; step++ {
reply, u, err := chatOnce(working)
...
if len(reply.ToolCalls) == 0 {
if reply.Content != "" {
onEvent(map[string]string{"type": "delta", "content": reply.Content})
}
return working, total, nil
}
这里比较关键的一点是ReAct,不是直接回答,而是推理(Reason)和 行动(Act)交替进行的多轮循环。
- 观察(Observe)输入模型的内容。(观察 系统提示词 + 用户问题:"今天是几号,星期几?" )
- 推理(Reason),LLM决定:先调用时间工具 get_current_time。
- 行动(Act),应用执行对应的get_current_time函数。
- 再观察(Observe),观察 系统提示词 + 用户问题 + get_current_time返回的时间数据。
- 推理(Reason),LLM给到问题的答案。
- 行动(Act),返回给用户答案。
上面这个是一个比较简单的例子,只循环了2次ReAct。
jsx
func runAgent(messages []Message, onEvent SSEEventHandler) ([]Message, usageTotals, error) {
working := append([]Message(nil), messages...)
var total usageTotals
for step := 1; step <= 8; step++ { // 上限 8 轮
reply, u, err := chatOnce(working) // 非流式,才能解析 tool_calls
// ... 累加 token ...
working = append(working, reply)
if len(reply.ToolCalls) == 0 { // 模型不再调工具 → 正常结束
if reply.Content != "" {
onEvent(map[string]string{"type": "delta", "content": reply.Content})
}
return working, total, nil
}
for _, call := range reply.ToolCalls {
onEvent(map[string]any{"type": "tool_start", "name": fn, "args": args})
result, _ := runTool(call.Function.Name, call.Function.Arguments)
onEvent(map[string]any{"type": "tool_done", "name": fn, "result": result.Text})
if result.FileURL != "" {
onEvent(map[string]any{"type": "file", "url": result.FileURL, "filename": result.FileName})
}
working = append(working, Message{Role: "tool", ToolCallID: call.ID, Content: result.Text})
}
}
return working, total, fmt.Errorf("超过最大工具调用步数(8)")
}
这里设置的最大步骤为8次,目前这个循环是线性执行的非常简单的任务,如果是更复杂的任务,可以使用LangChain、LangGraph之类的实现加强版循环。
二、根据当前季节生成菜谱
根据当前季节,给到一个当季的菜谱PPT,用户可以直接下载。这个例子包含了文件处理能力,工具的实现要复杂一些,比上一个例子的ReAct循环次数会多一点,但主要思路还是一样的。



