Agent 这个话题其实是近年来最火的话题之一,这篇文章就来讲讲如何用go语言通过 mcp+llm+rag 做一个agent demo。这个Demo将展示如何构建一个智能代理系统。
我们计划构建一个Agent Demo,该Demo将结合以下技术:
-
1、MCP(模型上下文协议):用于与大型语言模型(LLM)交互的协议。
-
2、LLM(大型语言模型):如OpenAI的GPT系列,我们将通过API调用。
-
3、RAG(检索增强生成):通过检索外部知识库来增强LLM的生成能力。
设计思路:
-
1、我们首先需要一个LLM,这里我们使用OpenAI的GPT模型,同时考虑到demo的简便性,我们使用OpenAI API。
-
2、为了实现RAG,我们需要一个向量数据库来存储和检索知识。这里我们使用Chroma(一个轻量级向量数据库,支持内存模式,适合demo)。
-
3、我们将构建一个简单的Agent,它能够接收用户的问题,然后通过RAG从向量数据库中检索相关知识,最后结合这些知识通过LLM生成答案。
实现步骤:
-
1、准备知识库:将文档(例如txt、pdf等)加载,并分割成片段,然后通过嵌入模型(embedding model)将文本片段转换为向量,并存储到向量数据库中。
-
2、构建检索器:当用户提问时,将用户的问题也通过相同的嵌入模型转换为向量,然后在向量数据库中检索最相关的文本片段。
-
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. 扩展功能建议
-
流式响应:支持SSE或WebSocket实现流式输出
-
多Agent协作:实现多个专业Agent协同工作
-
长期记忆:集成向量数据库保存对话历史
-
工具学习:Agent自动学习新工具的使用
-
监控和可观测性:集成OpenTelemetry和Prometheus
-
分布式部署:使用gRPC实现多实例部署
这个Agent Demo展示了:
-
MCP:通过标准化协议集成外部工具
-
LLM:支持多种模型提供智能推理
-
RAG:通过向量检索增强知识能力
-
工具系统:可扩展的工具调用机制
-
记忆管理:维护对话上下文
这个架构具有良好的扩展性,可以根据需要添加新的工具、模型和功能模块。