2025年的今天,大语言模型的能力早已不再限于chatbot(只会聊天),各家模型都在文本预测的基础上扩展LLM的能力,例如Structured Outputs(结构化输出),Function calling(函数调用),MCP(模型上下文协议),同时,有人提出一种结合LLM思考与行动的协同机制-React。

React Agent

通过让LLM循环执行 推理(Reasoning)->行动(Action)->观察(Observation) 来完成任务。 alt text 本质上,可以用下面这段最小代码解释:

1for {
2    response := callLLM(context)
3    if response.ToolCalls {
4        context = executeTools(response.ToolCalls)
5    }
6    if response.Finished { return }
7}

ReAct: Synergizing Reasoning and Acting in Language Models

eino-React实现

Function calling

LLM自身训练的结构化输出能力以及通过openai-api服务实现,让LLM能够按照指定格式输出json格式的数据,来表明自己需要去使用什么函数,参数是什么,同时支持流式生成。 例如: alt text 之后你应该在输入的messages里面加入除user,assistant的另一个角色,名为tool,tool的content内容为函数调用结果,至于函数是如何调用,LLM和openai-api服务并不参与,由开发者执行。此时再次对话,LLM会根据上下文中函数调用结果生成回答。

openai-function-calling

MCP

MCP (Model Context Protocol)是一种开放协议,用于标准化应用程序如何向大型语言模型(LLMs)提供上下文。可以将 MCP 想象为 AI 应用的 typec 接口。正如 typec 提供了一种标准化的方式将您的设备连接到各种外设和配件,MCP 也提供了一种标准化的方式,将 AI 模型连接到不同的数据源和工具。

以工具为例,我们可以通过 MCP Client 获取 MCP Server 提供的工具提供给LLM,并根据LLM所要调用的函数向 MCP Server发起远程函数调用,并将函数返回结果使用上文提到的方式加入上下文中。

MCP可以是函数定义与函数执行的一个实现方式

每个人都遵守MCP协议,就可以很方便将自己的写的函数(工具+)让别人的大模型接入调用,这也是协议的意义。

alt text 此处不再过多赘述,详情参考:

官方文档

OpenMcp-what-is-mcp

整合架构图

alt text 看完上文,你应该能对MCP,Function calling 与 React Agent 之间的关系有所了解。

实践

接下来我会用Golang以及openai-gomcp-go-sdk对上图各个部分实现一个最简MVP版本。

依赖:

1go get -u github.com/modelcontextprotocol/go-sdk
2go get -u github.com/openai/openai-go

MCP Server

包含一个最简的输入ip返回ip归属地信息的工具的 MCP Server SSE实现,可以通过docker一键运行。

github仓库

这个仓库使用的是mcp-go的v0.1.0版本,和目前最新的v0.2.0有很大不同。

部署完成后,推荐使用vscode插件openmcp进行工具调用测试。 alt text 实际上,openmcp也是上面架构图的实现。 alt text

MCP Client

初始化一个session

 1func main() {
 2	ctx := context.Background()
 3	mcpClient := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
 4	transport := mcp.NewSSEClientTransport(os.Getenv("TEST_MCP_BASE_URL"), nil)
 5	session, err := mcpClient.Connect(ctx, transport)
 6	if err != nil {
 7		log.Fatal(err)
 8	}
 9	defer session.Close()
10}

根据上图,MCP client主要负责提供函数定义和调用功能。

ListTools

从 MCP Server获取工具列表

1mcpTools, err := session.ListTools(ctx, nil)
2if err != nil {
3	log.Fatal(err)
4}

ToolCall

通过对应工具名称和输入参数调用远程的MCP tool。

1res, err := session.CallTool(ctx, &mcp.CallToolParams{
2	Name:      "ip-resolve",
3	Arguments: `{"ip": "198.199.77.16"}`,
4})
5if err != nil {
6	return nil, err
7}

ChatModel

这是整个React Agent的核心部分,最好的体验是流式会话。

1client := openai.NewClient(
2	option.WithBaseURL(os.Getenv("OPENAI_BASE_URL")),
3	option.WithAPIKey(os.Getenv("OPENAI_API_KEY")),
4)
5client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
6	Tools: []openai.ChatCompletionToolParam{},
7})

工具转换

MCP client获取到的工具列表需要转换成openai-go可以接受的工具列表格式。

 1func ToolsFormMCP(list *mcp.ListToolsResult) ([]openai.ChatCompletionToolParam, error) {
 2	var tools []openai.ChatCompletionToolParam
 3	for _, tool := range list.Tools {
 4		var toolParam openai.ChatCompletionToolParam
 5		data, err := json.Marshal(tool.InputSchema)
 6		if err != nil {
 7			return nil, err
 8		}
 9		if err := json.Unmarshal(data, &toolParam.Function.Parameters); err != nil {
10			return nil, err
11		}
12		toolParam.Function.Name = tool.Name
13		toolParam.Function.Description = openai.String(tool.Description)
14        //可选,严格模式
15		toolParam.Function.Strict = openai.Bool(true)
16		toolParam.Function.Parameters["additionalProperties"] = false
17		tools = append(tools, toolParam)
18	}
19	return tools, nil
20}

有关工具调用具体参数详见openai-api文档

流式会话

这里使用了Go1.23新增的迭代器模式简化,有关迭代器参考关于Golang新增的迭代器

acc可以用来收集本次完整响应msg。

不仅是生成的文本,函数调用的参数也是流式的,需要我们自行拼接。

 1var acc openai.ChatCompletionAccumulator
 2toolCalls := make(map[int64]*openai.ChatCompletionChunkChoiceDeltaToolCall)
 3stream := client.Chat.Completions.NewStreaming(ctx, params)
 4for chunk := range Chunks(stream) {
 5    acc.AddChunk(chunk)
 6	if len(chunk.Choices) < 1 {
 7		continue
 8	}
 9	content := chunk.Choices[0].Delta.Content
10
11    for _, call := range chunk.Choices[0].Delta.ToolCalls {
12				if _, ok := toolCalls[call.Index]; !ok {
13					toolCalls[call.Index] = &call
14					continue
15				}
16				toolCall := toolCalls[call.Index]
17				toolCall.Function.Arguments += call.Function.Arguments
18    }
19}
20newmsg := acc.ChatCompletion.Choices[0].Message.ToParam()
1func Chunks[T any](stream *ssestream.Stream[T]) iter.Seq[T] {
2	return func(yield func(T) bool) {
3		for stream.Next() {
4			if !yield(stream.Current()) {
5				return
6			}
7		}
8	}
9}

如何判断某个函数调用的参数已经拼接完成呢?我的办法是使用一个三方库gjson,gjson有一个方法可以判断一个string是否是合法的json字符串,如果合法,意味着该函数参数已经拼接完成。

1go get -u github.com/tidwall/gjson
1if gjson.Valid(toolCall.Function.Arguments) {
2    //根据参数调用函数
3}

工具调用(tool handler)

根据chat model输出的tool name与arguments,调用远程 MCP Server对应的函数。

 1func McpToolHandler(session *mcp.ClientSession) ToolHandle {
 2	return func(ctx context.Context, call openai.ChatCompletionChunkChoiceDeltaToolCallFunction) (*openai.ChatCompletionToolMessageParamContentUnion, error) {
 3		var args json.RawMessage
 4		if err := json.Unmarshal([]byte(call.Arguments), &args); err != nil {
 5			return nil, err
 6		}
 7
 8		res, err := session.CallTool(ctx, &mcp.CallToolParams{
 9			Name:      call.Name,
10			Arguments: args,
11		})
12		if err != nil {
13			return nil, err
14		}
15		content := ""
16		for _, c := range res.Content {
17			content += c.(*mcp.TextContent).Text
18		}
19		return &openai.ChatCompletionToolMessageParamContentUnion{
20			OfString: openai.String(content),
21		}, nil
22	}
23}

React Agent

前置准备完毕,现在可以循环Re起来了!每次调用完成后如果toolCalls不为0,向下一轮对话加入本次回复的msg与tool msgs,重新进行对话,直到toolCalls为0,结束React。

完整代码详见github仓库

alt text

总结

一个完整React Agent 配合 MCP tools,由纯Golang实现,现在很多教程都在讲MCP Server的实现,缺少完整的教程,所以这篇博客还是很有价值的。字节的eino框架也实现了一个React Agent,不过代码复杂,不适合快速学习。动手实现对于理解MCP Agent Function calling帮助很大!

参考

Model Context Protocol — Intuitively and Exhaustively Explained Why LangGraph Overcomplicates AI Agents (And My Go Alternative)