图解|Go语言实现 Agent|LLM+MCP+RAG
写在前面
agent 这个话题其实是近年来最火的话题之一,这篇文章就来讲讲如何用go语言通过 mcp+llm+rag 做一个agent demo。
代码都在github上 https://github.com/CocaineCong/llm-mcp-rag ,过段时间B站会出coding视频,感兴趣的同学可以关注同名的B站账号~
整体架构

- 将
Prompt和MCP Client 的所有 Tools以及RAG内容给到 LLM - LLM会根据
Prompt + Tools给出使用步骤以及 tools 名字和对应的参数 - 如果有多个MCP Client,要先找到对应的 Tool 是哪个MCP Client
- 接着 MCP Client 会调用 Call Tool 使用工具
- 真正执行是在 MCP Server 中,大模型不会调用,而是需要我们写代码进行 ToolCall
举个例子:如果是想爬取一个网页的内容

LLM
新建一个LLM,我们就选用 OpenAI 的 ChatGPT 了
- NewChatOpenAI:New一个LLM对象,必须要传一个model name
type ChatOpenAI struct {Ctx context.Context Model string // 模型名称Tools []mcp.Tool // 所需要的工具Message []openai.ChatCompletionMessageParamUnion // LLM的上下文信息(会话历史)LLM openai.Client // 具体的客户端
}func NewChatOpenAI(ctx context.Context, model string) *ChatOpenAI {options := []option.RequestOption{option.WithAPIKey(os.Getenv(ChatGPTOpenAPIKEY)),option.WithBaseURL(os.Getenv(ChatGPTBaseURL)),}cli := openai.NewClient(options...)llm := &ChatOpenAI{Ctx: ctx,Model: model,LLM: cli,Message: make([]openai.ChatCompletionMessageParamUnion, 0),}return llm
}
- Chat:实现具体和LLM进行Chat,而
这里我们 Stream 流式进行模型的通信。而这个Chat函数的返回值有两个,一个是模型的结果,另一个是模型让我们调用的工具。

func (c *ChatOpenAI) Chat(prompt string) (result string, toolCall []openai.ToolCallUnion) {// 将prompt保存到message中,作为模型的上下文通信的消息,相当于本次session的缓存动作if prompt != "" {c.Message = append(c.Message, openai.UserMessage(prompt))}// 将MCP Tool转为OpenAI所理解的Tool的格式toolsParam := MCPTool2OpenAITool(c.Tools)stream := c.LLM.Chat.Completions.NewStreaming(c.Ctx, openai.ChatCompletionNewParams{Messages: c.Message,Seed: openai.Int(0),Model: c.Model,Tools: toolsParam,})// 流式结果的累加器,用来将每个分片chunk拼接成完整的消息acc := openai.ChatCompletionAccumulator{}var toolCalls []openai.ToolCallUnion // LLM 返回的Tool的结果for stream.Next() {chunk := stream.Current() // 当前的stream流的chunk分片acc.AddChunk(chunk) // 解析每一个chunk,将chunk进行结构化// 模型完整生成的一个工具调用if tool, ok := acc.JustFinishedToolCall(); ok {toolCalls = append(toolCalls, openai.ToolCallUnion{ID: tool.ID,Function: openai.FunctionToolCallFunction{Name: tool.Name,Arguments: tool.Arguments,},})}// 模型所吐出的结果,一点一点进行拼接结果if len(chunk.Choices) > 0 {result += chunk.Choices[0].Delta.Content}}if len(acc.Choices) > 0 { // 将本次模型的最终回复写入会话历史c.Message = append(c.Message, acc.Choices[0].Message.ToParam())}return result, toolCalls
}
MCP
我们MCP用的是这个库 github.com/mark3labs/mcp-go,主要有三个组成部份。
- MCP 的Start启动连接,这会
拉起一个MCP Server进程并建立连接。 - MCP 的初始化 Initialize,这会进行
协议握手,双方进行确认。 - MCP 的 CallTool 将 ToolName 和 Args 发给MCP Server。

type MCPClient struct {Ctx context.Context // 上下文Client *client.Client // mcp的服务clientTools []mcp.Tool // mcp 工具Cmd string // 命令Args []string // 参数Env []string
}func NewMCPClient(ctx context.Context, cmd string, env, args []string) *MCPClient {stdioTransport := transport.NewStdio(cmd, env, args...) // 使用stdio进行连接通信cli := client.NewClient(stdioTransport)m := &MCPClient{Ctx: ctx, Client: cli, Cmd: cmd, Args: args, Env: env}return m
}
- Start:MCP Client的初始化
func (m *MCPClient) Start() error {// 负责把 MCP 服务端真正跑起来并连上err := m.Client.Start(m.Ctx)if err != nil {return err}mcpInitReq := mcp.InitializeRequest{}mcpInitReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSIONmcpInitReq.Params.ClientInfo = mcp.Implementation{Name: "example-client",Version: "0.0.1",}// 让服务端认可我们的客户端与协议版本,再进入“可工作”的状态。if _, err = m.Client.Initialize(m.Ctx, mcpInitReq); err != nil {fmt.Println("mcp init error:", err)return err}return err
}
- CallTool: MCP进行工具调用,也就是调用MCP Server,具体的实现是在MCP Server中实行。
func (m *MCPClient) CallTool(name string, args any) (string, error) {res, err := m.Client.CallTool(m.Ctx, mcp.CallToolRequest{Params: mcp.CallToolParams{Name: name,Arguments: args,},})if err != nil {return "", err}return mcp.GetTextFromContent(res.Content), nil
}
Agent
接下来到我们的agent,agent主要是整合了上面的LLM和MCP。

- NewAgent:新建一个Agent
- 激活所有的mcp client 拿到所有的tools
- 激活并告诉llm有哪些tools
type Agent struct {Ctx context.ContextMCPClient []*MCPClientLLM *ChatOpenAIModel string
}func NewAgent(ctx context.Context, model string, mcpCli []*MCPClient) *Agent {// 1. 激活所有的mcp client 拿到所有的toolstools := make([]mcp.Tool, 0)for _, item := range mcpCli {// 启动 stdio 传输err := item.Start()if err != nil {continue}err = item.SetTools()if err != nil {continue}tools = append(tools, item.GetTool()...)}// 2. 激活并告诉llm有哪些toolsllm := NewChatOpenAI(ctx, model, WithLLMTools(tools))return &Agent{Ctx: ctx,MCPClient: mcpCli,LLM: llm,Model: model,}
}
- Invoke:具体和LLM通信
- 从上面我们知道 LLM 的 Chat 方法会返回响应和所需要用到的工具 ToolCalls,ToolCalls 里面有对应的ToolName 和所需要的 Args。
- 根据ToolName找到对应的MCP Client,并进行CallTool
- CallTool就是调用
MCP Server去真正的干活
⚠️注意:大模型是不会帮我们执行的,大模型只会给出一个步骤(也就是 tool name 和 tool args),而这个执行步骤是需要我们去写的,也就是真正的MCP Server
核心代码逻辑如下:
func (a *Agent) Invoke(prompt string) string {if a.LLM == nil {return ""}response, toolCalls := a.LLM.Chat(prompt)for len(toolCalls) > 0 {// 省略了一些代码... 找到是哪个MCP Client,并进行CallToolif mcpTool.Name == toolCall.Function.Name {toolText, err := mcpClient.CallTool(toolCall.Function.Name, toolCall.Function.Arguments)if err != nil {continue}a.LLM.Message = append(a.LLM.Message, openai.ToolMessage(toolText, toolCall.ID))}}return response
}
运行结果:

