Go语言实现 Agent Demo

Agent 这个话题其实是近年来最火的话题之一,这篇文章就来讲讲如何用go语言通过 mcp+llm+rag 做一个agent demo。这个Demo将展示如何构建一个智能代理系统。

我们计划构建一个Agent Demo,该Demo将结合以下技术:

  1. 1、MCP(模型上下文协议):用于与大型语言模型(LLM)交互的协议。

  2. 2、LLM(大型语言模型):如OpenAI的GPT系列,我们将通过API调用。

  3. 3、RAG(检索增强生成):通过检索外部知识库来增强LLM的生成能力。

设计思路:

  1. 1、我们首先需要一个LLM,这里我们使用OpenAI的GPT模型,同时考虑到demo的简便性,我们使用OpenAI API。

  2. 2、为了实现RAG,我们需要一个向量数据库来存储和检索知识。这里我们使用Chroma(一个轻量级向量数据库,支持内存模式,适合demo)。

  3. 3、我们将构建一个简单的Agent,它能够接收用户的问题,然后通过RAG从向量数据库中检索相关知识,最后结合这些知识通过LLM生成答案。

实现步骤:

  1. 1、准备知识库:将文档(例如txt、pdf等)加载,并分割成片段,然后通过嵌入模型(embedding model)将文本片段转换为向量,并存储到向量数据库中。

  2. 2、构建检索器:当用户提问时,将用户的问题也通过相同的嵌入模型转换为向量,然后在向量数据库中检索最相关的文本片段。

  3. 3、构建Agent:将检索到的相关文本片段和用户问题一起构建提示(prompt),发送给LLM,让LLM生成答案。

完整Agent Demo实现

项目结构

html 复制代码
go-agent-demo/
├── main.go
├── go.mod
├── config.yaml
├── mcp/
│   ├── client.go
│   └── server.go
├── rag/
│   ├── vector_store.go
│   └── retriever.go
├── agent/
│   ├── core.go
│   └── executor.go
├── llm/
│   ├── openai.go
│   └── local.go
└── tools/
    ├── calculator.go
    └── web_search.go

1. 核心Agent实现

agent/core.go

Go 复制代码
package agent

import (
	"context"
	"fmt"
	"log"
	"strings"
	"time"

	"go-agent-demo/llm"
	"go-agent-demo/mcp"
	"go-agent-demo/rag"
	"go-agent-demo/tools"
)

type AgentConfig struct {
	LLMModel      string
	Temperature   float32
	MaxIterations int
	UseRAG        bool
	UseMCP        bool
}

type Agent struct {
	config      AgentConfig
	llmClient   llm.LLMClient
	ragRetriever *rag.Retriever
	mcpClient   *mcp.Client
	tools       map[string]tools.Tool
	memory      []MemoryItem
}

type MemoryItem struct {
	Query     string
	Response  string
	Timestamp time.Time
}

type AgentResponse struct {
	Response string
	Sources  []rag.Document
	UsedTools []string
}

func NewAgent(config AgentConfig) (*Agent, error) {
	// 初始化LLM客户端
	llmClient, err := llm.NewOpenAIClient(config.LLMModel)
	if err != nil {
		return nil, err
	}

	agent := &Agent{
		config:    config,
		llmClient: llmClient,
		tools:     make(map[string]tools.Tool),
		memory:    make([]MemoryItem, 0),
	}

	// 初始化RAG检索器
	if config.UseRAG {
		agent.ragRetriever = rag.NewRetriever()
	}

	// 初始化MCP客户端
	if config.UseMCP {
		mcpClient, err := mcp.NewClient("localhost:8081")
		if err != nil {
			log.Printf("MCP client init failed: %v", err)
		} else {
			agent.mcpClient = mcpClient
		}
	}

	// 注册工具
	agent.registerTools()

	return agent, nil
}

func (a *Agent) registerTools() {
	// 注册内置工具
	a.tools["calculator"] = &tools.Calculator{}
	a.tools["web_search"] = &tools.WebSearch{}
	
	// 通过MCP注册外部工具
	if a.mcpClient != nil {
		externalTools, err := a.mcpClient.GetTools()
		if err == nil {
			for _, tool := range externalTools {
				a.tools[tool.Name] = tool
			}
		}
	}
}

func (a *Agent) ProcessQuery(ctx context.Context, query string) (*AgentResponse, error) {
	// 添加到记忆
	a.memory = append(a.memory, MemoryItem{
		Query:     query,
		Timestamp: time.Now(),
	})

	// 步骤1: 如果启用RAG,先检索相关文档
	var ragDocs []rag.Document
	if a.config.UseRAG && a.ragRetriever != nil {
		docs, err := a.ragRetriever.Retrieve(query, 3)
		if err == nil && len(docs) > 0 {
			ragDocs = docs
			query = a.enrichQueryWithContext(query, docs)
		}
	}

	// 步骤2: 分析查询并决定使用哪些工具
	toolPlan, err := a.planToolUsage(query)
	if err != nil {
		return nil, err
	}

	// 步骤3: 执行工具调用
	results := a.executeTools(ctx, toolPlan)

	// 步骤4: 生成最终响应
	finalResponse, err := a.generateResponse(query, results, ragDocs)
	if err != nil {
		return nil, err
	}

	// 步骤5: 更新记忆
	a.updateMemory(query, finalResponse)

	return &AgentResponse{
		Response:  finalResponse,
		Sources:   ragDocs,
		UsedTools: getUsedToolNames(toolPlan),
	}, nil
}

func (a *Agent) planToolUsage(query string) ([]ToolCall, error) {
	prompt := fmt.Sprintf(`分析用户查询并决定使用哪些工具。
可用工具: %s
查询: %s

请以JSON格式返回:
{
  "thought": "思考过程",
  "tools": [
    {
      "name": "工具名",
      "input": "工具输入",
      "reason": "使用原因"
    }
  ]
}`, a.getToolDescriptions(), query)

	response, err := a.llmClient.Generate(prompt, a.config.Temperature)
	if err != nil {
		return nil, err
	}

	return parseToolPlan(response)
}

func (a *Agent) executeTools(ctx context.Context, toolCalls []ToolCall) map[string]interface{} {
	results := make(map[string]interface{})
	
	for _, toolCall := range toolCalls {
		if tool, exists := a.tools[toolCall.Name]; exists {
			result, err := tool.Execute(ctx, toolCall.Input)
			if err != nil {
				results[toolCall.Name] = fmt.Sprintf("Error: %v", err)
			} else {
				results[toolCall.Name] = result
			}
		}
	}
	
	return results
}

func (a *Agent) generateResponse(query string, toolResults map[string]interface{}, docs []rag.Document) (string, error) {
	contextStr := a.buildContextString(toolResults, docs)
	
	prompt := fmt.Sprintf(`基于以下信息和工具执行结果回答问题。

用户问题: %s

上下文信息:
%s

工具执行结果:
%s

请提供准确、有帮助的回答:`, query, contextStr, formatToolResults(toolResults))

	return a.llmClient.Generate(prompt, a.config.Temperature)
}

func (a *Agent) enrichQueryWithContext(query string, docs []rag.Document) string {
	contexts := make([]string, len(docs))
	for i, doc := range docs {
		contexts[i] = doc.Content
	}
	
	return fmt.Sprintf("基于以下信息: %s\n\n问题: %s", 
		strings.Join(contexts, "\n\n"), query)
}

2. MCP客户端实现

mcp/client.go

Go 复制代码
package mcp

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

type MCPClient struct {
	baseURL    string
	httpClient *http.Client
}

type Tool struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	InputSchema any    `json:"input_schema"`
	Execute     func(ctx context.Context, input any) (any, error)
}

func NewClient(baseURL string) (*MCPClient, error) {
	return &MCPClient{
		baseURL: baseURL,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}, nil
}

func (c *MCPClient) GetTools() ([]Tool, error) {
	resp, err := c.httpClient.Get(c.baseURL + "/tools")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var tools []Tool
	if err := json.NewDecoder(resp.Body).Decode(&tools); err != nil {
		return nil, err
	}

	return tools, nil
}

func (c *MCPClient) ExecuteTool(ctx context.Context, toolName string, input any) (any, error) {
	reqBody, err := json.Marshal(map[string]any{
		"tool":  toolName,
		"input": input,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, "POST", 
		c.baseURL+"/execute", 
		strings.NewReader(string(reqBody)))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}

	return result["result"], nil
}

3. RAG检索器实现

rag/retriever.go

Go 复制代码
package rag

import (
	"context"
	"encoding/json"
	"log"
	"os"
	"strings"

	"github.com/google/uuid"
	"github.com/qdrant/go-client/qdrant"
)

type Document struct {
	ID      string   `json:"id"`
	Content string   `json:"content"`
	Metadata map[string]string `json:"metadata"`
	Embedding []float32 `json:"embedding"`
}

type Retriever struct {
	vectorStore VectorStore
	embedder    Embedder
}

func NewRetriever() *Retriever {
	// 使用Qdrant作为向量存储
	store, err := NewQdrantStore("localhost:6334")
	if err != nil {
		log.Printf("Failed to init vector store: %v", err)
		store = NewMemoryStore() // 降级到内存存储
	}

	return &Retriever{
		vectorStore: store,
		embedder:    NewOpenAIEmbedder(),
	}
}

func (r *Retriever) AddDocuments(docs []Document) error {
	for i := range docs {
		if docs[i].ID == "" {
			docs[i].ID = uuid.New().String()
		}
		
		// 生成嵌入向量
		if len(docs[i].Embedding) == 0 {
			embedding, err := r.embedder.Embed(docs[i].Content)
			if err != nil {
				return err
			}
			docs[i].Embedding = embedding
		}
	}
	
	return r.vectorStore.Upsert(docs)
}

func (r *Retriever) Retrieve(query string, k int) ([]Document, error) {
	// 生成查询的嵌入向量
	queryEmbedding, err := r.embedder.Embed(query)
	if err != nil {
		return nil, err
	}
	
	// 相似性搜索
	return r.vectorStore.Search(queryEmbedding, k)
}

func (r *Retriever) LoadFromFile(filepath string) error {
	data, err := os.ReadFile(filepath)
	if err != nil {
		return err
	}
	
	var docs []Document
	if err := json.Unmarshal(data, &docs); err != nil {
		return err
	}
	
	return r.AddDocuments(docs)
}

// 文本分割器
func SplitText(text string, chunkSize int, overlap int) []string {
	var chunks []string
	words := strings.Fields(text)
	
	if len(words) <= chunkSize {
		return []string{text}
	}
	
	for i := 0; i < len(words); i += chunkSize - overlap {
		end := i + chunkSize
		if end > len(words) {
			end = len(words)
		}
		chunk := strings.Join(words[i:end], " ")
		chunks = append(chunks, chunk)
		
		if end == len(words) {
			break
		}
	}
	
	return chunks
}

4. LLM客户端抽象

llm/openai.go

Go 复制代码
package llm

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

type LLMClient interface {
	Generate(prompt string, temperature float32) (string, error)
	GenerateWithContext(context, prompt string) (string, error)
}

type OpenAIClient struct {
	apiKey     string
	baseURL    string
	model      string
	httpClient *http.Client
}

func NewOpenAIClient(model string) (*OpenAIClient, error) {
	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("OPENAI_API_KEY not set")
	}
	
	return &OpenAIClient{
		apiKey:     apiKey,
		baseURL:    "https://api.openai.com/v1",
		model:      model,
		httpClient: &http.Client{},
	}, nil
}

func (c *OpenAIClient) Generate(prompt string, temperature float32) (string, error) {
	request := CompletionRequest{
		Model:       c.model,
		Messages:    []Message{{Role: "user", Content: prompt}},
		Temperature: temperature,
		MaxTokens:   2000,
	}
	
	resp, err := c.makeRequest(request)
	if err != nil {
		return "", err
	}
	
	if len(resp.Choices) == 0 {
		return "", fmt.Errorf("no completion choices returned")
	}
	
	return resp.Choices[0].Message.Content, nil
}

func (c *OpenAIClient) makeRequest(req CompletionRequest) (*CompletionResponse, error) {
	body, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	
	httpReq, err := http.NewRequest("POST", 
		c.baseURL+"/chat/completions",
		bytes.NewBuffer(body))
	if err != nil {
		return nil, err
	}
	
	httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
	httpReq.Header.Set("Content-Type", "application/json")
	
	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API request failed: %s", resp.Status)
	}
	
	var completionResp CompletionResponse
	if err := json.NewDecoder(resp.Body).Decode(&completionResp); err != nil {
		return nil, err
	}
	
	return &completionResp, nil
}

// 支持本地模型
type LocalLLMClient struct {
	modelPath string
	// 可以集成 llama.cpp 或其他本地推理引擎
}

func NewLocalClient(modelPath string) (*LocalLLMClient, error) {
	return &LocalLLMClient{
		modelPath: modelPath,
	}, nil
}

5. 工具系统

tools/calculator.go

Go 复制代码
package tools

import (
	"context"
	"fmt"
	"strconv"
	"strings"
)

type Tool interface {
	Name() string
	Description() string
	Execute(ctx context.Context, input any) (any, error)
}

type Calculator struct{}

func (c *Calculator) Name() string {
	return "calculator"
}

func (c *Calculator) Description() string {
	return "执行数学计算,支持加减乘除和括号"
}

func (c *Calculator) Execute(ctx context.Context, input any) (any, error) {
	expr, ok := input.(string)
	if !ok {
		return nil, fmt.Errorf("input must be string")
	}
	
	// 简单的表达式求值(生产环境应使用更安全的求值器)
	result, err := c.evaluate(expr)
	if err != nil {
		return nil, err
	}
	
	return map[string]any{
		"expression": expr,
		"result":     result,
	}, nil
}

func (c *Calculator) evaluate(expr string) (float64, error) {
	// 简化实现,实际应使用表达式解析库
	parts := strings.Fields(expr)
	if len(parts) != 3 {
		return 0, fmt.Errorf("invalid expression format")
	}
	
	a, err := strconv.ParseFloat(parts[0], 64)
	if err != nil {
		return 0, err
	}
	
	b, err := strconv.ParseFloat(parts[2], 64)
	if err != nil {
		return 0, err
	}
	
	switch parts[1] {
	case "+":
		return a + b, nil
	case "-":
		return a - b, nil
	case "*":
		return a * b, nil
	case "/":
		if b == 0 {
			return 0, fmt.Errorf("division by zero")
		}
		return a / b, nil
	default:
		return 0, fmt.Errorf("unsupported operator: %s", parts[1])
	}
}

6. 主程序

main.go

Go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"

	"go-agent-demo/agent"
	"go-agent-demo/rag"
	"gopkg.in/yaml.v3"
)

type Config struct {
	Agent agent.AgentConfig `yaml:"agent"`
	RAG   struct {
		Enabled     bool   `yaml:"enabled"`
		DataPath    string `yaml:"data_path"`
		ChunkSize   int    `yaml:"chunk_size"`
	} `yaml:"rag"`
	Server struct {
		Port int `yaml:"port"`
	} `yaml:"server"`
}

func main() {
	// 加载配置
	config, err := loadConfig("config.yaml")
	if err != nil {
		log.Fatal("Failed to load config:", err)
	}

	// 初始化Agent
	agent, err := agent.NewAgent(config.Agent)
	if err != nil {
		log.Fatal("Failed to create agent:", err)
	}

	// 加载RAG数据
	if config.RAG.Enabled {
		retriever := agent.GetRetriever()
		if retriever != nil {
			err := retriever.LoadFromFile(config.RAG.DataPath)
			if err != nil {
				log.Printf("Warning: Failed to load RAG data: %v", err)
			} else {
				log.Println("RAG data loaded successfully")
			}
		}
	}

	// 启动HTTP服务器
	go startHTTPServer(agent, config.Server.Port)

	// 交互式命令行
	go startCLI(agent)

	// 等待终止信号
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	<-sigChan
	
	log.Println("Shutting down...")
}

func startCLI(agent *agent.Agent) {
	fmt.Println("Agent CLI Started. Type 'exit' to quit.")
	fmt.Println("========================================")
	
	ctx := context.Background()
	
	for {
		fmt.Print("\n> ")
		var input string
		fmt.Scanln(&input)
		
		if input == "exit" {
			break
		}
		
		response, err := agent.ProcessQuery(ctx, input)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			continue
		}
		
		fmt.Printf("\nAgent: %s\n", response.Response)
		if len(response.Sources) > 0 {
			fmt.Println("\nSources:")
			for _, source := range response.Sources {
				fmt.Printf("- %s\n", source.Metadata["title"])
			}
		}
		if len(response.UsedTools) > 0 {
			fmt.Printf("\nUsed tools: %v\n", response.UsedTools)
		}
	}
}

func loadConfig(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	
	var config Config
	if err := yaml.Unmarshal(data, &config); err != nil {
		return nil, err
	}
	
	return &config, nil
}

7. 配置文件

config.yaml

Go 复制代码
agent:
  llm_model: "gpt-4"  # 或 "gpt-3.5-turbo", "claude-3", "local:llama2"
  temperature: 0.7
  max_iterations: 5
  use_rag: true
  use_mcp: true

rag:
  enabled: true
  data_path: "./data/knowledge_base.json"
  chunk_size: 500
  top_k: 3

server:
  port: 8080

mcp:
  servers:
    - url: "http://localhost:8081"
    - url: "http://localhost:8082"
  
tools:
  enabled:
    - calculator
    - web_search
    - sql_query
    - file_reader

8. 部署和运行

go.mod

Go 复制代码
module go-agent-demo

go 1.21

require (
    github.com/qdrant/go-client v0.4.0
    github.com/google/uuid v1.6.0
    gopkg.in/yaml.v3 v3.0.1
)

Dockerfile

bash 复制代码
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o agent-demo .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/agent-demo .
COPY config.yaml .
COPY data/ ./data/
EXPOSE 8080
CMD ["./agent-demo"]

9. 扩展功能建议

  1. 流式响应:支持SSE或WebSocket实现流式输出

  2. 多Agent协作:实现多个专业Agent协同工作

  3. 长期记忆:集成向量数据库保存对话历史

  4. 工具学习:Agent自动学习新工具的使用

  5. 监控和可观测性:集成OpenTelemetry和Prometheus

  6. 分布式部署:使用gRPC实现多实例部署

这个Agent Demo展示了:

  • MCP:通过标准化协议集成外部工具

  • LLM:支持多种模型提供智能推理

  • RAG:通过向量检索增强知识能力

  • 工具系统:可扩展的工具调用机制

  • 记忆管理:维护对话上下文

这个架构具有良好的扩展性,可以根据需要添加新的工具、模型和功能模块。

相关推荐
传奇开心果编程16 分钟前
【springboot基础语法学与练】第 1 课:从零开始
java·spring boot·后端·学习
Bruce_Liuxiaowei20 分钟前
Python 实例赋值遮蔽类属性:一个从不报错的静默陷阱
开发语言·python·语法糖
IT_陈寒25 分钟前
Java线程池用错参数,我的服务居然悄悄崩溃了
前端·人工智能·后端
光影少年36 分钟前
Express 中间件原理
后端·node.js·express
aramae2 小时前
MySQL内置函数(7)
开发语言·笔记·后端·mysql·其他
徐小夕2 小时前
存量.doc 文档怎么办?JitWord 开源转换库打通旧文档到在线协同全链路
后端·架构·github
ee又momo2 小时前
一次 JWT Token 过期续期的踩坑记录
后端
五彩小白3 小时前
GO语言装饰器语法
golang
二月龙3 小时前
App 瘦身实战:图片、so 库、资源压缩,安装包体积减小 40%
后端
大勇前进3 小时前
Kotlin 协程实战避坑:90% 的人都会踩的生命周期泄漏问题
后端