2025年的今天,大语言模型的能力早已不再限于chatbot(只会聊天),各家模型都在文本预测的基础上扩展LLM的能力,例如Structured Outputs(结构化输出),Function calling(函数调用),MCP(模型上下文协议),同时,有人提出一种结合LLM思考与行动的协同机制-React。
React Agent
通过让LLM循环执行 推理(Reasoning)->行动(Action)->观察(Observation) 来完成任务。
本质上,可以用下面这段最小代码解释:
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
Function calling
LLM自身训练的结构化输出能力以及通过openai-api服务实现,让LLM能够按照指定格式输出json格式的数据,来表明自己需要去使用什么函数,参数是什么,同时支持流式生成。
例如:
之后你应该在输入的messages里面加入除user,assistant的另一个角色,名为tool,tool的content内容为函数调用结果,至于函数是如何调用,LLM和openai-api服务并不参与,由开发者执行。此时再次对话,LLM会根据上下文中函数调用结果生成回答。
MCP
MCP (Model Context Protocol)是一种开放协议,用于标准化应用程序如何向大型语言模型(LLMs)提供上下文。可以将 MCP 想象为 AI 应用的 typec 接口。正如 typec 提供了一种标准化的方式将您的设备连接到各种外设和配件,MCP 也提供了一种标准化的方式,将 AI 模型连接到不同的数据源和工具。
以工具为例,我们可以通过 MCP Client 获取 MCP Server 提供的工具提供给LLM,并根据LLM所要调用的函数向 MCP Server发起远程函数调用,并将函数返回结果使用上文提到的方式加入上下文中。
MCP可以是函数定义与函数执行的一个实现方式
每个人都遵守MCP协议,就可以很方便将自己的写的函数(工具+)让别人的大模型接入调用,这也是协议的意义。
此处不再过多赘述,详情参考:
整合架构图
看完上文,你应该能对MCP,Function calling 与 React Agent 之间的关系有所了解。
实践
接下来我会用Golang以及openai-go与mcp-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一键运行。
这个仓库使用的是mcp-go的v0.1.0版本,和目前最新的v0.2.0有很大不同。
部署完成后,推荐使用vscode插件openmcp进行工具调用测试。
实际上,openmcp也是上面架构图的实现。
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仓库
总结
一个完整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)