AI文章生成 - Eino集成DeepSeek


03 - AI 文章生成 — Eino 集成 DeepSeek


本章目标

  • 了解 CloudWeGo Eino 框架的核心抽象
  • 创建 DeepSeek ChatModel 并调用
  • 设计 System Prompt 模板
  • 解析 AI 返回的文章内容(标题 + 标签提取)
  • 将 AgentService 注册为 Wails Service

3.1 为什么选择 Eino

在 Go 生态中调用大模型,你有几种选择:

方案 优点 缺点
直接 HTTP 调用 OpenAI 兼容 API 简单直接 需要自己处理重试、流式、上下文管理
使用模型官方 SDK 功能完整 锁定模型供应商
CloudWeGo Eino 统一抽象,可切换模型 学习曲线稍高

Eino 是字节跳动开源的 AI 应用开发框架。它的 ChatModel 接口统一了不同大模型的调用方式:

// Eino 的 ChatModel 接口(简化版)
type BaseChatModel interface {
    Generate(ctx context.Context, messages []*Message) (*Message, error)
    Stream(ctx context.Context, messages []*Message) (*MessageStream, error)
}

📌 关键概念: 使用 Eino 后,从 DeepSeek 切换到 OpenAI、通义千问、文心一言等,只需要换一个 ChatModel 实现,业务代码无需修改。


3.2 添加依赖

go get github.com/cloudwego/eino@latest
go get github.com/cloudwego/eino-ext/components/model/deepseek@latest

查看 go.mod,确认新增了类似以下依赖:

require (
    github.com/cloudwego/eino v0.8.4
    github.com/cloudwego/eino-ext/components/model/deepseek v0.0.0-20251117090452-bd6375a0b3cf
    ...
)

3.3 创建 AgentService

创建 backend/agent.go

package backend

import (
    "context"
    "fmt"

    "github.com/cloudwego/eino-ext/components/model/deepseek"
    "github.com/cloudwego/eino/components/model"
    "github.com/cloudwego/eino/schema"
)

请求和响应结构体

// ArtRequest 内容创作请求
type ArtRequest struct {
    Topic      string `json:"topic"`       // 写作主题
    PetType    string `json:"pet_type"`    // 宠物类型(如:猫、狗、兔子等)
    Style      string `json:"style"`       // 写作风格(科普、故事、指南等)
    Keywords   string `json:"keywords"`    // 关键词提示
    WordCount  int    `json:"word_count"`  // 目标字数
    ImageCount int    `json:"image_count"` // 配图数量
}

// ArticleResult 创作结果
type ArticleResult struct {
    Title      string   `json:"title"`
    Content    string   `json:"content"`
    SearchTags []string `json:"search_tags"` // AI 提取的关键词
}

AgentService 结构体

// AgentService AI 内容创作服务
type AgentService struct {
    configService *ConfigService
}

// NewAgentService 创建 Agent 服务
func NewAgentService(cs *ConfigService) *AgentService {
    return &AgentService{configService: cs}
}

3.4 创建 DeepSeek ChatModel

// createChatModel 创建 DeepSeek ChatModel
func (as *AgentService) createChatModel(ctx context.Context) (model.BaseChatModel, error) {
    cfg, err := as.configService.Load()
    if err != nil {
        return nil, fmt.Errorf("加载配置失败: %w", err)
    }
    if cfg.DeepSeekAPIKey == "" {
        return nil, fmt.Errorf("请先配置 DeepSeek API Key")
    }

    temperature := float32(0.7)
    maxTokens := 4096
    modelName := cfg.DeepSeekModel
    if modelName == "" {
        modelName = "deepseek-chat"
    }

    chatModel, err := deepseek.NewChatModel(ctx, &deepseek.ChatModelConfig{
        APIKey:      cfg.DeepSeekAPIKey,
        Model:       modelName,
        Temperature: temperature,
        MaxTokens:   maxTokens,
    })
    if err != nil {
        return nil, fmt.Errorf("创建 DeepSeek 模型失败: %w", err)
    }
    return chatModel, nil
}

参数说明

参数 说明 推荐值
Temperature 随机性 0-2,越高越有创意,越低越确定 内容创作建议 0.7-0.9
MaxTokens 最大输出 token 数,中英文混合约 1.5 字符/token 800 字文章设 4096 足够
Model 模型名称 deepseek-chat(通用)或 deepseek-reasoner(推理)

3.5 设计 System Prompt

System Prompt 是 AI 创作的灵魂。一个高质量的 Prompt 需要包含:

  1. 角色设定 — 你是谁
  2. 任务描述 — 你要做什么
  3. 约束条件 — 怎么做的规则
  4. 输出格式 — 以什么结构输出
// buildSystemPrompt 构建系统提示词
func buildSystemPrompt(req ArtRequest) string {
    petType := req.PetType
    if petType == "" {
        petType = "宠物"
    }
    wordCount := req.WordCount
    if wordCount <= 0 {
        wordCount = 800
    }
    style := req.Style
    if style == "" {
        style = "科普"
    }

    return fmt.Sprintf(`你是一个专业的今日头条宠物内容创作者,精通宠物知识科普写作。

【写作要求】
- 宠物类型:%s
- 写作风格:%s
- 目标字数:约 %d 字
- 写作风格要求:今日头条风格,标题吸引眼球但真实,内容通俗易懂、专业准确
- 文章结构:引人入胜的标题 → 开篇引入 → 分点科普 → 总结建议
- 语言要求:接地气但不低俗,专业但不晦涩,适合普通宠物主人阅读

【关键词提取要求】
文章写完后,请在文章末尾用 "[TAGS]" 标签附上 3-5 个最适合搜索配图的关键词(英文,用逗号分隔)。
如:[TAGS] golden retriever puppy, pet care, dog training

请确保内容原创、准确、有价值。`, petType, style, wordCount)
}

💡 知识点: Prompt 中的 [TAGS] 是一个自定义的结构化标记,用于后续程序化解析 AI 输出。这比让 AI 返回 JSON 更可靠(AI 可能生成格式不正确的 JSON),也比纯文本更易于解析。


3.6 调用模型生成文章

// GenerateArticle 生成文章(前端调用)
func (as *AgentService) GenerateArticle(ctx context.Context, req ArtRequest) (*ArticleResult, error) {
    // 1. 创建 ChatModel
    chatModel, err := as.createChatModel(ctx)
    if err != nil {
        return nil, err
    }

    // 2. 构建消息
    messages := []*schema.Message{
        {
            Role:    schema.System,
            Content: buildSystemPrompt(req),
        },
        {
            Role:    schema.User,
            Content: fmt.Sprintf("请根据以上要求,创作一篇关于%s的宠物%s文章。\n主题:%s\n关键词提示:%s",
                req.PetType, req.Style, req.Topic, req.Keywords),
        },
    }

    // 3. 调用模型生成
    resp, err := chatModel.Generate(ctx, messages)
    if err != nil {
        return nil, fmt.Errorf("AI 生成失败: %w", err)
    }

    // 4. 解析结果:分离文章内容和搜索标签
    title, content, tags := parseArticleResponse(resp.Content, req.Topic)

    return &ArticleResult{
        Title:      title,
        Content:    content,
        SearchTags: tags,
    }, nil
}

消息角色说明

角色 含义 用法
schema.System 系统级指令 设定 AI 的身份、行为、输出格式
schema.User 用户消息 具体的创作请求
schema.Assistant AI 回复 多轮对话中使用(本项目不需要)

⚠️ 注意: DeepSeek 的 System Prompt 权重很高。如果你写的 System Prompt 不清晰,AI 的输出可能会偏离预期。建议在正式使用前多测试几版 Prompt。


3.7 解析 AI 返回结果

AI 返回的是一整段文本。我们需要从中提取:

  1. 标题 — 文章第一行(或 # 开头的行)
  2. 正文[TAGS] 之前的所有内容
  3. 搜索标签[TAGS] 之后逗号分隔的英文关键词
// parseArticleResponse 解析 AI 返回的文章,提取标题、正文和搜索标签
func parseArticleResponse(raw string, fallbackTopic string) (title, content string, tags []string) {
    // 查找 [TAGS] 标签
    tagsIdx := -1
    for i := 0; i < len(raw); i++ {
        if i+6 <= len(raw) && raw[i:i+6] == "[TAGS]" {
            tagsIdx = i
            break
        }
    }

    if tagsIdx >= 0 {
        content = raw[:tagsIdx]
        tagStr := raw[tagsIdx+6:]
        // 按逗号或换行分割标签
        for _, t := range splitByComma(tagStr) {
            t = trim(t)
            if t != "" {
                tags = append(tags, t)
            }
        }
    } else {
        content = raw
        // AI 没有返回标签时,用默认标签兜底
        tags = []string{"pet care", "cute pets", "animal health"}
    }

    // 提取标题
    title = extractTitle(content, fallbackTopic)
    return
}

标题提取逻辑

func extractTitle(content string, fallback string) string {
    lines := splitByNewline(content)
    // 优先取 # 开头的行作为标题
    for _, line := range lines {
        line = trim(line)
        if len(line) > 2 && line[0] == '#' {
            return trimLeft(line, "# ")
        }
    }
    // 否则取第一个非空行
    for _, line := range lines {
        line = trim(line)
        if line != "" && len(line) > 5 {
            return line
        }
    }
    return fallback
}

💡 知识点: 这些字符串处理函数(splitByCommasplitByNewlinetrimtrimLeft)都是手工实现的,没有使用 strings 包。这是为了展示 Go 的字符串底层操作。实际项目中推荐直接使用 strings 包。


3.8 注册到 main.go

func main() {
    configService := backend.NewConfigService()
    agentService := backend.NewAgentService(configService)

    app := application.New(application.Options{
        Name: "pet-content-creator",
        Services: []application.Service{
            application.NewService(configService),
            application.NewService(agentService),
        },
        // ...
    })
    // ...
}

3.9 测试 AI 生成

你可以先写一个快速测试脚本(不经过 UI)来验证 AI 生成效果:

// backend/agent_test.go
package backend

import (
    "context"
    "testing"
)

func TestGenerateArticle(t *testing.T) {
    cs := NewConfigService()
    as := NewAgentService(cs)

    ctx := context.Background()
    req := ArtRequest{
        Topic:     "如何让猫咪多喝水",
        PetType:   "猫",
        Style:     "科普",
        WordCount: 500,
    }

    result, err := as.GenerateArticle(ctx, req)
    if err != nil {
        t.Fatalf("生成失败: %v", err)
    }

    t.Logf("标题: %s", result.Title)
    t.Logf("标签: %v", result.SearchTags)
    t.Logf("内容前 200 字: %s...", result.Content[:200])
}

运行测试:

# 确保配置了 API Key
go test ./backend -run TestGenerateArticle -v

⚠️ 注意: 测试会真实调用 DeepSeek API 并产生费用。建议设较小的 MaxTokens 值。


本章总结

你已经学会 对应能力
CloudWeGo Eino ChatModel 抽象 统一模型调用接口
DeepSeek ChatModel 创建与配置 调用国产大模型
System Prompt 设计 控制 AI 输出质量
[TAGS] 结构化标记 可编程解析 AI 输出
标题 + 标签提取 文本后处理

🔧 动手练习

  1. 修改 System Prompt,让 AI 生成不同风格的文章(如小红书风格、知乎风格)
  2. 将 Temperature 调整为 0.3 和 1.2,对比生成效果
  3. 增加 [SUMMARY] 标记,让 AI 在文章开头生成一段 50 字摘要
  4. 实现错误重试逻辑:如果 AI 返回了 [TAGS] 但标签为空,自动重试一次

👉 下一章:免费图库集成 — Pexels API 实战

wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo