🛠️ 实战练习手册
🎯 练习目标
通过动手实践,深入理解 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 实现定期清理
- 性能优化:通过缓存减少重复计算
🏆 综合项目:构建专业级助手
最终挑战:实现一个代码助手
功能要求
- 代码分析工具:检查 Go 代码质量
- 文档生成工具:为函数生成文档
- 测试生成工具:为函数生成单元测试
- 重构建议工具:提供代码改进建议
技术要求
- 使用
go/ast解析 Go 代码 - 集成
golint、go vet等工具 - 实现智能的代码模式识别
- 提供交互式的重构建议
评估标准
- 功能完整性:是否实现所有要求的工具
- 代码质量:代码结构、错误处理、测试覆盖
- 用户体验:界面友好、响应迅速、错误信息清晰
- 扩展性:是否易于添加新功能
📊 练习完成检查表
基础练习
- 练习一:URL缩短工具开发
- 练习二:智能路由优化
- 练习三:上下文感知对话
- 练习四:性能监控与缓存
进阶练习
- 集成真实的外部API
- 实现配置文件管理
- 添加用户认证功能
- 实现多语言支持
高级挑战
- 构建代码助手项目
- 实现分布式部署
- 性能压测与优化
- 开源项目贡献
🎓 学习反思
完成练习后,请思考以下问题:
- 架构设计:如何设计可扩展的工具系统?
- 性能优化:哪些地方可以进一步优化性能?
- 用户体验:如何让用户更容易使用这个系统?
- 错误处理:如何优雅地处理各种异常情况?
- 测试策略:如何确保代码质量和系统稳定性?
🎯 通过这些实战练习,您将深入掌握 langchain-go 的核心技术,并具备构建专业级 AI 应用的能力!