Skip to main content

🛠️ 实战练习手册

🎯 练习目标

通过动手实践,深入理解 langchain-go 的核心概念和项目架构设计。


📝 练习一:基础工具开发

任务:实现一个 URL 缩短工具

1.1 需求分析

  • 功能:将长URL转换为短URL
  • 输入:长URL地址
  • 输出:短URL或错误信息
  • 应用场景:链接分享、文档整理

1.2 实现步骤

步骤1:创建工具文件

touch tool/url_shortener_tool.go

步骤2:实现工具接口

package tool

import (
"context"
"fmt"
"net/url"
"strings"
"crypto/md5"
)

type URLShortenerTool struct{}

func (u URLShortenerTool) Name() string {
return "url_shortener"
}

func (u URLShortenerTool) Description() string {
return "缩短长URL链接,输入完整的URL地址"
}

func (u URLShortenerTool) Call(ctx context.Context, input string) (string, error) {
// TODO: 在这里实现您的逻辑
return "", nil
}

// 辅助函数:验证URL格式
func (u URLShortenerTool) validateURL(rawURL string) error {
// TODO: 实现URL验证逻辑
return nil
}

// 辅助函数:生成短URL
func (u URLShortenerTool) generateShortURL(longURL string) string {
// TODO: 实现短URL生成算法
return ""
}

步骤3:添加到工具列表

// 在 cmd/chat.go 中添加
availableTools := []tools.Tool{
tool.CalculatorTool{},
tool.WeatherTool{},
tool.TimeTool{},
tool.SummaryTool{},
tool.URLShortenerTool{}, // 新增
}

步骤4:添加识别规则

// 在 identifyTool 函数中添加
if strings.Contains(inputLower, "缩短") || strings.Contains(inputLower, "短链接") ||
strings.Contains(inputLower, "shorten") {
return "url_shortener", input
}

1.3 参考实现

func (u URLShortenerTool) Call(ctx context.Context, input string) (string, error) {
// 提取URL
rawURL := strings.TrimSpace(input)

// 验证URL格式
if err := u.validateURL(rawURL); err != nil {
return "", fmt.Errorf("URL格式无效: %v", err)
}

// 生成短URL
shortURL := u.generateShortURL(rawURL)

return fmt.Sprintf("原链接: %s\n短链接: %s", rawURL, shortURL), nil
}

func (u URLShortenerTool) validateURL(rawURL string) error {
_, err := url.Parse(rawURL)
if err != nil {
return err
}

if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
return fmt.Errorf("URL必须以http://或https://开头")
}

return nil
}

func (u URLShortenerTool) generateShortURL(longURL string) string {
// 简单的MD5哈希算法
hash := md5.Sum([]byte(longURL))
return fmt.Sprintf("https://short.ly/%x", hash[:4])
}

1.4 测试验证

# 编译项目
go build -o chat-app .

# 运行测试
./chat-app chat

# 测试输入
🧑 缩短 https://github.com/tmc/langchaingo
🧑 短链接 https://www.example.com/very/long/path/to/page

📚 学习要点

  • 接口实现:理解 tools.Tool 接口的三个方法
  • 输入验证:学会处理用户输入的边界情况
  • 错误处理:提供有意义的错误信息
  • 算法实现:掌握简单的哈希算法应用

📝 练习二:智能路由优化

任务:实现基于优先级的工具匹配

2.1 需求分析

当前的工具识别使用简单的关键词匹配,存在以下问题:

  • 关键词冲突(如"时间计算"既包含"时间"又包含"计算")
  • 无法处理复杂表达(如"现在北京的天气和时间")
  • 缺乏优先级排序

2.2 设计方案

方案1:评分机制

type ToolMatcher struct {
ToolName string
Keywords []string
Weights map[string]int // 关键词权重
MinScore int // 最小匹配分数
}

func (tm *ToolMatcher) Score(input string) int {
score := 0
inputLower := strings.ToLower(input)

for _, keyword := range tm.Keywords {
if strings.Contains(inputLower, keyword) {
weight := tm.Weights[keyword]
if weight == 0 {
weight = 1 // 默认权重
}
score += weight
}
}

return score
}

方案2:正则表达式匹配

type RegexMatcher struct {
ToolName string
Patterns []string // 正则表达式模式
}

func (rm *RegexMatcher) Match(input string) (bool, map[string]string) {
for _, pattern := range rm.Patterns {
re := regexp.MustCompile(pattern)
if matches := re.FindStringSubmatch(input); len(matches) > 0 {
// 返回匹配结果和捕获组
groups := make(map[string]string)
for i, name := range re.SubexpNames() {
if i != 0 && name != "" && i < len(matches) {
groups[name] = matches[i]
}
}
return true, groups
}
}
return false, nil
}

2.3 实现任务

步骤1:创建新的路由器

// 在 cmd/chat.go 中创建
type SmartRouter struct {
matchers []ToolMatcher
}

func NewSmartRouter() *SmartRouter {
return &SmartRouter{
matchers: []ToolMatcher{
{
ToolName: "weather",
Keywords: []string{"天气", "气温", "温度", "weather"},
Weights: map[string]int{"天气": 10, "气温": 8, "温度": 6, "weather": 10},
MinScore: 8,
},
{
ToolName: "current_time",
Keywords: []string{"时间", "几点", "现在", "当前", "time"},
Weights: map[string]int{"时间": 10, "几点": 10, "现在": 5, "当前": 5, "time": 10},
MinScore: 8,
},
// TODO: 添加更多匹配器
},
}
}

func (sr *SmartRouter) Route(input string) (toolName, toolInput string) {
bestMatch := ""
bestScore := 0

for _, matcher := range sr.matchers {
score := matcher.Score(input)
if score >= matcher.MinScore && score > bestScore {
bestMatch = matcher.ToolName
bestScore = score
}
}

if bestMatch != "" {
return bestMatch, sr.extractInput(input, bestMatch)
}

return "", ""
}

func (sr *SmartRouter) extractInput(input, toolName string) string {
// TODO: 根据工具类型提取相关输入
return input
}

步骤2:替换原有路由逻辑

// 替换 processWithChain 中的 identifyTool 调用
func processWithChain(llm *ollama.LLM, toolMap map[string]tools.Tool, input string) string {
router := NewSmartRouter()
toolName, toolInput := router.Route(input)

if toolName != "" {
if tool, exists := toolMap[toolName]; exists {
return callToolDirectly(tool, toolInput)
}
}

return callLLMChain(llm, input)
}

2.4 测试用例

// 创建测试文件 cmd/router_test.go
func TestSmartRouter(t *testing.T) {
router := NewSmartRouter()

testCases := []struct {
input string
expectedTool string
}{
{"北京的天气怎么样", "weather"},
{"现在几点了", "current_time"},
{"时间和天气", ""}, // 应该无匹配,因为分数相近
{"计算当前时间", "current_time"}, // 时间权重更高
}

for _, tc := range testCases {
toolName, _ := router.Route(tc.input)
if toolName != tc.expectedTool {
t.Errorf("输入: %s, 期望: %s, 实际: %s", tc.input, tc.expectedTool, toolName)
}
}
}

📚 学习要点

  • 算法优化:从简单匹配到评分机制
  • 数据结构:合理组织配置数据
  • 测试驱动:通过测试验证改进效果
  • 可扩展性:便于添加新的匹配规则

📝 练习三:提示工程优化

任务:实现上下文感知的对话系统

3.1 需求分析

当前系统每次对话都是独立的,缺乏上下文记忆:

  • 无法记住用户偏好
  • 无法进行连续对话
  • 缺乏个性化回复

3.2 设计方案

会话管理器

type ConversationManager struct {
sessions map[string]*Session
mutex sync.RWMutex
}

type Session struct {
ID string
Messages []Message
Context map[string]any
Created time.Time
Updated time.Time
}

type Message struct {
Role string // "user" | "assistant"
Content string
Timestamp time.Time
Metadata map[string]any
}

3.3 实现步骤

步骤1:创建会话管理器

// 创建文件 cmd/conversation.go
package cmd

import (
"fmt"
"sync"
"time"
)

func NewConversationManager() *ConversationManager {
return &ConversationManager{
sessions: make(map[string]*Session),
}
}

func (cm *ConversationManager) GetOrCreateSession(sessionID string) *Session {
cm.mutex.Lock()
defer cm.mutex.Unlock()

session, exists := cm.sessions[sessionID]
if !exists {
session = &Session{
ID: sessionID,
Messages: make([]Message, 0),
Context: make(map[string]any),
Created: time.Now(),
Updated: time.Now(),
}
cm.sessions[sessionID] = session
}

return session
}

func (s *Session) AddMessage(role, content string) {
message := Message{
Role: role,
Content: content,
Timestamp: time.Now(),
Metadata: make(map[string]any),
}

s.Messages = append(s.Messages, message)
s.Updated = time.Now()

// 保持最近N条消息
if len(s.Messages) > 10 {
s.Messages = s.Messages[len(s.Messages)-10:]
}
}

func (s *Session) GetRecentMessages(count int) []Message {
if count > len(s.Messages) {
count = len(s.Messages)
}
return s.Messages[len(s.Messages)-count:]
}

步骤2:增强提示模板

func buildContextualPrompt(session *Session, input string) string {
// 获取最近3条消息作为上下文
recentMessages := session.GetRecentMessages(3)

contextBuilder := strings.Builder{}
contextBuilder.WriteString("最近对话历史:\n")

for _, msg := range recentMessages {
contextBuilder.WriteString(fmt.Sprintf("%s: %s\n", msg.Role, msg.Content))
}

// 构建完整提示
template := `你是一个友好的AI助手,能够记住对话历史并提供连贯的回复。

{{.context}}

用户偏好信息:
{{.preferences}}

当前用户输入:{{.input}}

请基于对话历史和用户偏好,提供自然、连贯的回复:`

prompt := strings.ReplaceAll(template, "{{.context}}", contextBuilder.String())
prompt = strings.ReplaceAll(prompt, "{{.input}}", input)
prompt = strings.ReplaceAll(prompt, "{{.preferences}}", getPreferences(session))

return prompt
}

func getPreferences(session *Session) string {
// 从会话上下文中提取用户偏好
if prefs, exists := session.Context["preferences"]; exists {
return fmt.Sprintf("%v", prefs)
}
return "暂无偏好信息"
}

步骤3:集成到主流程

// 修改 chatCmd 的 Run 函数
Run: func(cmd *cobra.Command, args []string) {
// 初始化组件
llm, _ := ollama.New(ollama.WithModel("llama3.2"))
toolMap := buildToolMap()
conversationManager := NewConversationManager()

// 创建或获取会话
sessionID := "default" // 实际中可以根据用户生成
session := conversationManager.GetOrCreateSession(sessionID)

fmt.Println("🤖 智能助手已启动!(支持上下文记忆)")

for {
fmt.Print("🧑 ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)

if input == "quit" || input == "exit" {
break
}

// 添加用户消息到会话
session.AddMessage("user", input)

// 处理请求(传入会话上下文)
response := processWithContext(llm, toolMap, session, input)

// 添加助手回复到会话
session.AddMessage("assistant", response)

fmt.Printf("🤖 %s\n\n", response)
}
},

步骤4:实现上下文处理函数

func processWithContext(llm *ollama.LLM, toolMap map[string]tools.Tool, session *Session, input string) string {
// 首先尝试工具识别
if toolName, toolInput := identifyTool(input); toolName != "" {
if tool, exists := toolMap[toolName]; exists {
result := callToolDirectly(tool, toolInput)

// 更新会话上下文
updateSessionContext(session, toolName, toolInput, result)

return result
}
}

// 使用上下文感知的LLM对话
return callContextualLLMChain(llm, session, input)
}

func updateSessionContext(session *Session, toolName, input, result string) {
// 记录工具使用历史
toolHistory, _ := session.Context["tool_history"].([]map[string]string)
toolHistory = append(toolHistory, map[string]string{
"tool": toolName,
"input": input,
"result": result,
"time": time.Now().Format("15:04:05"),
})
session.Context["tool_history"] = toolHistory

// 根据工具类型更新偏好
if toolName == "weather" {
session.Context["preferred_city"] = extractCityName(input)
}
}

func callContextualLLMChain(llm *ollama.LLM, session *Session, input string) string {
// 构建包含上下文的提示
promptText := buildContextualPrompt(session, input)
prompt := prompts.NewPromptTemplate(promptText, []string{})

// 创建和执行链
chain := chains.NewLLMChain(llm, prompt)
result, err := chains.Call(context.Background(), chain, map[string]any{})

if err != nil {
return fmt.Sprintf("⚠️ 对话处理失败: %v", err)
}

if text, ok := result["text"]; ok {
return fmt.Sprintf("%v", text)
}

return "抱歉,我没能理解您的问题。"
}

3.4 测试场景

# 测试连续对话
🧑 我叫张三
🤖 你好张三!很高兴认识你...

🧑 北京的天气怎么样
🤖 北京今日天气:晴转多云,气温 15-25°C,微风

🧑 那上海呢
🤖 上海今日天气:小雨,气温 18-22°C,东南风

🧑 我明天要去哪个城市比较好
🤖 根据天气情况,我建议你明天去北京,那里是晴天...

📚 学习要点

  • 状态管理:维护会话状态和上下文信息
  • 内存管理:控制消息历史长度,避免内存泄漏
  • 提示工程:利用历史信息构建更好的提示
  • 用户体验:通过上下文感知提升交互质量

📝 练习四:性能优化与监控

任务:实现性能监控和缓存系统

4.1 需求分析

  • 监控工具调用性能
  • 缓存重复查询结果
  • 统计使用情况

4.2 实现步骤

步骤1:性能监控

// 创建文件 cmd/metrics.go
type PerformanceMonitor struct {
metrics map[string]*ToolMetrics
mutex sync.RWMutex
}

type ToolMetrics struct {
CallCount int64
TotalTime time.Duration
ErrorCount int64
LastError error
LastCallTime time.Time
}

func (pm *PerformanceMonitor) RecordCall(toolName string, duration time.Duration, err error) {
pm.mutex.Lock()
defer pm.mutex.Unlock()

metrics, exists := pm.metrics[toolName]
if !exists {
metrics = &ToolMetrics{}
pm.metrics[toolName] = metrics
}

metrics.CallCount++
metrics.TotalTime += duration
metrics.LastCallTime = time.Now()

if err != nil {
metrics.ErrorCount++
metrics.LastError = err
}
}

func (pm *PerformanceMonitor) GetStats() map[string]ToolMetrics {
pm.mutex.RLock()
defer pm.mutex.RUnlock()

stats := make(map[string]ToolMetrics)
for name, metrics := range pm.metrics {
stats[name] = *metrics
}
return stats
}

步骤2:缓存系统

// 创建文件 cmd/cache.go
type ResponseCache struct {
cache map[string]CacheEntry
mutex sync.RWMutex
ttl time.Duration
}

type CacheEntry struct {
Value string
Timestamp time.Time
}

func NewResponseCache(ttl time.Duration) *ResponseCache {
cache := &ResponseCache{
cache: make(map[string]CacheEntry),
ttl: ttl,
}

// 启动清理协程
go cache.cleanup()

return cache
}

func (rc *ResponseCache) Get(key string) (string, bool) {
rc.mutex.RLock()
defer rc.mutex.RUnlock()

entry, exists := rc.cache[key]
if !exists {
return "", false
}

if time.Since(entry.Timestamp) > rc.ttl {
return "", false
}

return entry.Value, true
}

func (rc *ResponseCache) Set(key, value string) {
rc.mutex.Lock()
defer rc.mutex.Unlock()

rc.cache[key] = CacheEntry{
Value: value,
Timestamp: time.Now(),
}
}

func (rc *ResponseCache) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()

for range ticker.C {
rc.mutex.Lock()
now := time.Now()
for key, entry := range rc.cache {
if now.Sub(entry.Timestamp) > rc.ttl {
delete(rc.cache, key)
}
}
rc.mutex.Unlock()
}
}

4.3 集成任务

将监控和缓存集成到主系统中,实现:

  • 工具调用性能统计
  • 天气查询结果缓存(避免重复API调用)
  • 定期输出性能报告

📚 学习要点

  • 并发编程:使用 sync.RWMutex 保护共享数据
  • 后台任务:使用 goroutine 实现定期清理
  • 性能优化:通过缓存减少重复计算

🏆 综合项目:构建专业级助手

最终挑战:实现一个代码助手

功能要求

  1. 代码分析工具:检查 Go 代码质量
  2. 文档生成工具:为函数生成文档
  3. 测试生成工具:为函数生成单元测试
  4. 重构建议工具:提供代码改进建议

技术要求

  • 使用 go/ast 解析 Go 代码
  • 集成 golintgo vet 等工具
  • 实现智能的代码模式识别
  • 提供交互式的重构建议

评估标准

  • 功能完整性:是否实现所有要求的工具
  • 代码质量:代码结构、错误处理、测试覆盖
  • 用户体验:界面友好、响应迅速、错误信息清晰
  • 扩展性:是否易于添加新功能

📊 练习完成检查表

基础练习

  • 练习一:URL缩短工具开发
  • 练习二:智能路由优化
  • 练习三:上下文感知对话
  • 练习四:性能监控与缓存

进阶练习

  • 集成真实的外部API
  • 实现配置文件管理
  • 添加用户认证功能
  • 实现多语言支持

高级挑战

  • 构建代码助手项目
  • 实现分布式部署
  • 性能压测与优化
  • 开源项目贡献

🎓 学习反思

完成练习后,请思考以下问题:

  1. 架构设计:如何设计可扩展的工具系统?
  2. 性能优化:哪些地方可以进一步优化性能?
  3. 用户体验:如何让用户更容易使用这个系统?
  4. 错误处理:如何优雅地处理各种异常情况?
  5. 测试策略:如何确保代码质量和系统稳定性?

🎯 通过这些实战练习,您将深入掌握 langchain-go 的核心技术,并具备构建专业级 AI 应用的能力!