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:通过向量检索增强知识能力

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

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

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

相关推荐
萧鼎18 分钟前
Python 包管理的“超音速”革命:全面上手 uv 工具链
开发语言·python·uv
源代码•宸37 分钟前
大厂技术岗面试之谈薪资
经验分享·后端·面试·职场和发展·golang·大厂·职级水平的薪资
Anastasiozzzz1 小时前
Java Lambda 揭秘:从匿名内部类到底层原理的深度解析
java·开发语言
刘琦沛在进步1 小时前
【C / C++】引用和函数重载的介绍
c语言·开发语言·c++
机器视觉的发动机1 小时前
AI算力中心的能耗挑战与未来破局之路
开发语言·人工智能·自动化·视觉检测·机器视觉
HyperAI超神经1 小时前
在线教程|DeepSeek-OCR 2公式/表格解析同步改善,以低视觉token成本实现近4%的性能跃迁
开发语言·人工智能·深度学习·神经网络·机器学习·ocr·创业创新
晚霞的不甘1 小时前
CANN 编译器深度解析:UB、L1 与 Global Memory 的协同调度机制
java·后端·spring·架构·音视频
R_.L1 小时前
【QT】常用控件(按钮类控件、显示类控件、输入类控件、多元素控件、容器类控件、布局管理器)
开发语言·qt
喵叔哟1 小时前
06-ASPNETCore-WebAPI开发
服务器·后端·c#
Zach_yuan1 小时前
自定义协议:实现网络计算器
linux·服务器·开发语言·网络