基于 DeepSeek 的 AI 智能阅读助手开发实践

前言

在当今信息爆炸的时代,快速而高效地阅读文档和整理信息变得极其重要。专业人士、学生和学术研究者通常需要阅读大量的资料,而这些文档往往篇幅冗长、内容专业,需要耗费大量时间才能完全理解。特别是面对技术文档、学术论文或行业报告时,即使是领域专家也常常需要反复阅读才能掌握核心内容。

随着 AI 技术的发展,我们是否可以让它帮忙提升阅读效率?当然可以!

我开发了一款 AI 智能阅读助手 ,它是一个 浏览器插件 ,能够阅读网页内容和 PDF 文件,然后通过 AI 进行即时问答,甚至还能和它多轮对话,深入理解内容。

本文将详细介绍 AI 智能阅读助手的 项目概述、技术架构、核心功能实现,以及 如何借助腾讯云 DeepSeek APIAI 在阅读场景里发挥最大作用。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

项目概述

功能介绍

AI 智能阅读助手 是一款 浏览器插件,具备以下核心功能:

  • 网页内容解析与问答 :提取网页文本,并基于腾讯云 DeepSeek API 进行即时问答。

  • PDF 解析与内容问答 :上传 PDF 文件,AI 解析内容,并提供精准问答。

  • 多轮对话与上下文理解 :支持与 AI 进行多轮交互,理解用户意图,实现更自然的阅读体验。

  • 历史对话:支持查看历史对话并继续问答。

应用场景

  • 学生与研究人员:快速阅读论文,提取关键信息,进行文献问答。

  • 职场人士:高效浏览行业报告和领域文章,获取精准数据和分析。

效果演示

阅读当前网页文档

阅读PDF文件

历史对话

技术架构

AI 智能阅读助手前端浏览器插件 + 后端 API 服务 组成:

  • 前端(浏览器插件) :负责内容传递、用户交互。核心框架:WXT

  • 后端(Go API) :提供智能问答、文本解析等 AI 对话能力。核心技术:GoGinMongoDB 以及 腾讯云-知识引擎原子能力

腾讯云 DeepSeek API 在助手中的应用

腾讯云 DeepSeek API 介绍

腾讯云知识引擎原子能力 提供了 DeepSeek API 接口,我们可以通过腾讯云提供的 SDK 进行调用。同时,腾讯云还额外封装了DeepSeek OpenAI 对话接口,兼容了 OpenAI 的接口规范,这意味着我们可以直接使用 OpenAI 官方提供的 SDK 来调用。仅需要将 base_urlapi_key 替换成相关配置,不需要对应用做额外修改。

计费说明:

  • DeepSeek-R1 模型 | 输入:0.004 元 / 千 token | 输出(含思维链):0.016 元 / 千 token
  • DeepSeek-V3 模型 | 输入:0.002 元/ 千 token | 输出:0.008 元 / 千 token

腾讯云 DeepSeek API 的作用

腾讯云 DeepSeek API助手 里的主要作用包括:

  • 内容总结:快速对文档进行总结,提高阅读效率。

  • 即时问答:迅速回答用户的问题。

  • 多轮对话:保持上下文,支持深入理解用户意图。

腾讯云大模型知识引擎的实时文档解析 API 的应用

腾讯云大模型知识引擎的实时文档解析 API 支持将图片或PDF文件转换成Markdown格式文件,可解析包括表格、公式、图片、标题、段落、页眉、页脚等内容元素,并将内容智能转换成阅读顺序。

实时文档解析 API 在助手里的作用:

  • PDF 文档解析:将 PDF 文档内容转成大模型更易于理解的 Markdown 结构化的格式。

核心功能实现

即时问答与多轮交互

前端与后端通信(SSE 实现流式输出)

腾讯云 DeepSeek API 支持流式响应,为了提升对话体验,AI 智能阅读助手在前端采用 Server-Sent Events(SSE) 方式与后端进行通信,以便 AI 的响应能够以流式形式逐步返回,实现打字机的效果,而不是一次性加载整个回答。这样,我们就可以更快地看到 AI 生成的内容,提高交互的流畅性。

核心代码实现如下所示:

发起对话
typescript 复制代码
const completions = async (message: string) => {
  if (!chatId.value) return;


  const requestBody = {
    prompt: message,
    ...(props.sessionData?.type === 'webpage' && message === '' && { web_url: props.sessionData.source }),
    ...(props.sessionData?.type === 'file' && message === '' &&{

      media: {
        doc_type: props.sessionData.fileType,

        file_name: props.sessionData.fileName,
        path: props.sessionData.source
      }
    })
  };


  try {
    const response = await fetch(`${API_BASE}/chats/${chatId.value}/completions`, {
      method: 'POST',

      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);

    if (!response.body) throw new Error('无法获取响应流');


    const aiMessageId = generateId();
    messages.value.push({
      id: aiMessageId,
      content: '',
      reasoningContent: '',
      isUser: false,
      timestamp: new Date(),

      isComplete: false,
      showReasoning: false
    });

    await processSSEStream(response.body.getReader(), aiMessageId);
  } catch (error) {
    console.error('API请求失败:', error);

    showError(DEFAULT_ERROR_MSG);
    throw error;

  }
};

代码解释:

  • 初始验证检查

    • 检查对话 ID 是否存在,如果不存在则终止函数执行

    • 确保只在有效会话中进行对话 API 的调用

  • 构建请求体

    • 创建包含用户消息的请求对象

    • 根据会话类型动态添加不同参数:

      • 网页分析:当消息为空时添加网页 URL

      • 文件分析:当消息为空时添加文件类型、名称和路径

  • 发起API请求

    • 使用 fetch 向后端 API 发送 POST 请求

    • 设置适当的请求头和序列化的请求体

  • 错误处理

    • 检查 HTTP响应状态

    • 验证响应是否包含可读取的数据流

  • 创建新的消息体

    • 生成唯一的消息 ID

    • 向消息列表添加初始为空的 AI 回复

    • 设置初始状态标记(未完成、不显示推理过程等)

  • 处理 SSE数据流

    • 调用 processSSEStream函数处理响应流

    • 传入流读取器和 AI 消息 ID

    • 实时更新 UI上的AI回复内容

  • 异常捕获

    • 捕获并记录任何 API请求过程中的错误

    • 向用户显示友好的错误提示

    • 将错误向上传播以便进一步处理

处理流式响应
typescript 复制代码
const processSSEStream = async (reader: ReadableStreamDefaultReader, aiMessageId: string) => {
  currentReader.value = reader;
  isGenerating.value = true;
  const decoder = new TextDecoder();
  let buffer = '';

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;


      buffer += decoder.decode(value, { stream: true });

      while (true) {
        const eventEndIndex = buffer.indexOf('\n\n');
        if (eventEndIndex === -1) break;

        const eventData = buffer.substring(0, eventEndIndex);
        buffer = buffer.substring(eventEndIndex + 2);

        const lines = eventData.split('\n').filter(line => line.trim());
        let eventType = 'message';
        let jsonData = '';


        for (const line of lines) {
          if (line.startsWith('event:')) {

            eventType = line.substring(6).trim();
          } else if (line.startsWith('data:')) {
            jsonData += line.substring(5).trim();

          }
        }

        if (eventType === 'close') {
          try {
            const closeData = JSON.parse(jsonData);
            if (closeData.error) showError(`服务端关闭连接: ${closeData.error}`);
          } catch (e) {
            console.error('关闭事件解析失败:', e);
          }
          return;

        }

        if (jsonData) {
          try {
            const data = JSON.parse(jsonData) as ChatMessage;
            const targetIndex = messages.value.findIndex(m => m.id === aiMessageId);
            if (targetIndex === -1) return;

            const original = messages.value[targetIndex];
            
            // 处理reasoningContent和content的打字机效果

            messages.value[targetIndex] = {
              ...original,
              content: data.content ? original.content + data.content : original.content,
              reasoningContent: data.reasoning_content ? 
                (original.reasoningContent || '') + data.reasoning_content : 

                original.reasoningContent,
              timestamp: new Date((data.created_at || Date.now()) * 1000),
              showReasoning: original.showReasoning || !!data.reasoning_content
            };
            scrollToBottom();
          } catch (e) {

            console.error('SSE数据解析失败:', e, '原始数据:', jsonData);
          }
        }
      }
    }
  } catch (error) {
    console.error('读取流数据失败:', error);
    showError('连接异常中断,请重试');
  } finally {

    reader.releaseLock();
    currentReader.value = null;

    isLoading.value = false;

    isGenerating.value = false;
    const targetIndex = messages.value.findIndex(m => m.id === aiMessageId);
    if (targetIndex > -1) messages.value[targetIndex].isComplete = true;
  }
};

代码解释:

  • 初始化

    • 记录当前流读取器,并标记 AI 正在生成内容。

    • 创建解码器和缓冲区,用于处理流式数据。

  • 读取和解析 SSE 数据流

    • 通过 reader.read() 持续读取数据并解码。

    • 使用缓冲区存储未完全解析的数据,并按 \n\n 解析完整的 SSE 事件。

    • 识别事件类型 (messageclose),提取 data 部分。

  • 处理关闭事件

    • 如果服务器发送 close 事件,检查是否包含错误信息,并向用户提示错误。

    • 终止数据流处理。

  • 更新消息内容

    • 查找 aiMessageId 对应的消息对象,并将新内容追加,实现打字机效果。

    • 处理 reasoningContent(推理内容),更新时间戳,并决定是否显示推理信息。

    • 滚动到底部,确保用户看到最新消息。

  • 异常处理

    • 处理 JSON 解析失败或数据读取异常,并向用户显示错误提示。
  • 清理资源

    • 释放流读取器,重置状态,标记 AI 生成完成。

后端 API 处理(Go + Gin + SSE)

后端采用 Go Gin 框架 处理前端请求,并通过 SSE 返回流式数据。核心代码实现如下所示:

go 复制代码
streamData, err := h.serv.ChatCompletion(ctx, userId, chatPrompt)

if err != nil {
	slog.Error(err.Error())
	if errors.Is(err, mongo.ErrNoDocuments) {
		ctx.SSEvent("close", apiwrap.NewErrorResponseBody(404, "chat not exist"))
		return
	} else if errors.Is(err, errors.New("user is chatting")) {
		ctx.SSEvent("close", apiwrap.NewErrorResponseBody(400, "user is chatting"))
		return
	}
	ctx.SSEvent("close", apiwrap.NewResponseBody[any](500, err.Error(), nil))
	return
}
resp, _ := streamData.(*lkeap.ChatCompletionsResponse)
message := domain.ChatMessage{
	CreatedAt: time.Now(),

}

defer func() {
	_ = h.serv.SaveChatMessage(ctx, userId, chatId, message)
}()


for {
	select {
	case <-ctx.Request.Context().Done():
		// 客户端主动断开连接

		slog.Info("客户端断开连接")
		return
	case event, ok := <-resp.Events:

		if !ok {
			// 事件通道关闭,退出循环
			ctx.SSEvent("close", apiwrap.SuccessResponse())
			return
		}
		data := tclkep.ChatCompletionsResponse{}
		err = json.Unmarshal(event.Data, &data)
		if err != nil {
			ctx.SSEvent("close", apiwrap.NewResponseBody[any](500, err.Error(), nil))
			return
		}
		choice := data.Choices[0]
		message.Role = "assistant"
		message.Content += choice.Delta.Content
		message.ReasoningContent += choice.Delta.ReasoningContent
		cm := ChatMessage{
			Role:             choice.Delta.Role,
			Content:          choice.Delta.Content,
			ReasoningContent: choice.Delta.ReasoningContent,
			CreatedAt:        data.Created,
		}
		ctx.SSEvent("message", cm)
		ctx.Writer.Flush()
		slog.Info("chat completion response: ", cm)
	}
}

代码解释:

  • 处理请求并调用聊天服务

    • 通过 h.serv.ChatCompletion(ctx, userId, chatPrompt) 调用聊天服务,获取流式响应数据。

    • 如果请求失败,根据错误类型返回不同的 SSE 事件 close(例如:聊天不存在、用户正在聊天等)。

  • 初始化响应消息

    • 解析 streamData,并创建 message 结构体用于存储 AI 回复。

    • 使用 defer 关键字确保在函数返回前,持久化 message 数据到数据库。

  • 监听 SSE 事件并发送响应

    • 进入 for 循环,不断从 resp.Events 读取 AI 生成的内容。

    • 处理客户端断开:

      • 监听 ctx.Request.Context().Done(),如果客户端主动断开,则退出循环。
    • 解析 AI 返回的数据:

      • resp.Events 读取 event,如果通道关闭,发送 close 事件并终止循环。

      • 使用 json.Unmarshal(event.Data, &data) 解析 JSON 数据,提取 Choices 内容。

    • 更新并发送 AI 回复:

      • 解析 choice.Delta,更新 message.Contentmessage.ReasoningContent

      • 组装 ChatMessage 结构体,发送 SSE 事件 message,并调用 ctx.Writer.Flush() 确保数据立即推送到前端。

  • 终止逻辑

    • 发生错误时,发送 close 事件,返回错误信息。

    • resp.Events 关闭时,发送 close 事件并退出循环。

    • 日志记录每次 AI 生成的响应,便于调试和监控。

多轮交互实现(MongoDB 记录历史对话)

为了让 AI 能够理解对话上下文,每次用户发送消息时,后端系统需要 查询 MongoDB 里的历史对话 ,并将其与新问题封装后发送给 DeepSeek API

存储聊天记录
  • 采用 MongoDB 存储每个用户的对话记录。

  • 每条消息存入 chats 集合,并记录 用户 ID、对话标题,对话消息,对话时间 等信息

MongoDB 数据结构示例:

json 复制代码
{
    "_id": {"$oid": "67d799fc32fa24462017e415"},
    "created_at": {"$date": "2025-03-17T03:41:48.941Z"},
    "title": "Go 语言 mongox 库:简化操作、安全、高效、可扩展、BSON 构建.pdf",
    "updated_at": {"$date": "2025-03-17T03:42:46.570Z"},

    "user_id": "chenmingyong",
    "messages": [

      {
        "_id": {"$oid": "67d799fc32fa24462017e315"},

        "role": "system",
        "content": "\n你是一位专业的文档助手,负责为用户提供关于文档的详细、准确和清晰的回答。你的任务是帮助用户理解、分析和解决与文档相关的任何问题。\n任务目标: \n1. 回答问题 :针对用户提出的问题,提供详细、准确和易于理解的回答。 \n2. 保持专业性 :在回答问题时,始终保持专业、礼貌和客观的态度。 \n回答要求:\n1. 简洁明了 :回答应简洁明了,避免冗长和不必要的细节。 \n2. 结构化 :使用段落、列表或标题来组织信息,确保易于阅读和理解。 \n3. 引用文档 :在回答中引用文档中的具体部分,以支持你的观点或解释。 \n4. 提供上下文 :如果问题涉及文档的特定部分,提供足够的上下文信息,帮助用户理解。\n注意事项: \n1. 准确性 :确保所有回答都基于文档中的实际内容,避免猜测或假设。 \n2. 用户友好 :使用用户易于理解且与文档关联语言,避免使用超纲或复杂的术语。\n",
        "created_at": {"$date": "2025-03-17T03:41:57.648Z"},
        "is_hidden": true
      },
      {
        "content": "\n# 问题描述\n  \n帮我总结一下文档的内容,字数不超过 300 字。\n# 文档\n\n",
        "created_at": {"$date": "2025-03-17T03:41:59.728Z"},
        "is_hidden": false,
        "_id": {"$oid": "67d799fc32fa24462017e215"}
      }
    ]
  }
  
组装对话历史并发送给 DeepSeek API

当用户发送新的问题时,后端会从 MongoDB 获取所有历史对话信息,然后将历史对话合并,之后发送给 DeepSeek API,以保持上下文对话能力。

核心代码实现如下所示:

go 复制代码
// 查询历史对话
chat, err := s.repo.FindChatByUserIdAndChatId(ctx, userId, chatPrompt.ChatId)

if err != nil {
	return nil, err
}

// 封装新的对话信息

if len(chat.Messages) == 0 {
	chat.Messages = append(chat.Messages, domain.ChatMessage{
		Role:      "system",
		Content:   promptInfo.SystemPrompt,
		CreatedAt: now,
		IsHidden:  true,
	}, domain.ChatMessage{
		Role:      "user",
		Content:   promptInfo.UserPrompt,
		CreatedAt: now,
		IsHidden:  true,
	})
} else {
	chat.Messages = append(chat.Messages, domain.ChatMessage{
		Role:      "user",
		Content:   promptInfo.UserPrompt,
		CreatedAt: now,
	})
}

messages := slice.Map(chat.Messages, func(idx int, message domain.ChatMessage) *lkeap.Message {
	return &lkeap.Message{
		Content: gkit.ToPtr(message.Content),
		Role:    gkit.ToPtr(message.Role),
	}
})

// 与大模型进行对话
resp, err := s.client.NewChatCompletions("deepseek-r1").
	WithStream(true).
	WithMessages(messages).
	Do()
    

腾讯云 SDK 封装

虽然腾讯云提供的 SDK 能够帮助我们快速接入 API,但为了更符合我自己的编码习惯,我对其进行进一步封装。代码示例如下所示:

  • client 对象封装

    go 复制代码
    package tclkep
    
    import (
        "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
        "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
        lkeap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lkeap/v20240522"
    )
    
    // Client 封装腾讯云 LKEAP 客户端
    type Client struct {
        client *lkeap.Client
    }
    
    // NewLKEAPClient 创建一个新的 LKEAP 客户端
    func NewLKEAPClient(secretId, secretKey, region string) (*Client, error) {
        // 实例化一个认证对象
        credential := common.NewCredential(secretId, secretKey)
        // 实例化一个client选项
        cpf := profile.NewClientProfile()
        cpf.HttpProfile.Endpoint = "lkeap.tencentcloudapi.com"
        // 实例化要请求产品的client对象
    
        client, err := lkeap.NewClient(credential, region, cpf)
        if err != nil {
            return nil, err
        }
        return &Client{client: client}, nil
    }
    
    // NewChatCompletions 创建一个新的聊天完成请求对象
    func (c *Client) NewChatCompletions(model string) *ChatCompletions {
        request := lkeap.NewChatCompletionsRequest()
        request.Model = common.StringPtr(model)
        return &ChatCompletions{
            client:  c,
            request: request,
        }
    }
    
    func (c *Client) NewDocumentParser() *DocumentParser {
        request := lkeap.NewReconstructDocumentSSERequest()
        return &DocumentParser{
            client:  c,
            request: request,
        }
    }
  • 腾讯云 DeepSeek API 调用封装

    go 复制代码
    package tclkep
    
    import (
        "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
        lkeap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lkeap/v20240522"
    )
    
    // ChatCompletions 聊天完成接口的构建器,支持流式调用
    type ChatCompletions struct {
        client  *Client
        request *lkeap.ChatCompletionsRequest
    }
    
    // WithMessages 设置消息
    func (b *ChatCompletions) WithMessages(messages []*lkeap.Message) *ChatCompletions {
        b.request.Messages = messages
        return b
    }
    
    // AddMessage 添加单条消息
    func (b *ChatCompletions) AddMessage(role, content string) *ChatCompletions {
        message := &lkeap.Message{Role: common.StringPtr(role), Content: common.StringPtr(content)}
        if b.request.Messages == nil {
            b.request.Messages = []*lkeap.Message{message}
        } else {
            b.request.Messages = append(b.request.Messages, message)
        }
        return b
    
    }
    
    
    // WithStream 设置是否使用流式响应
    func (b *ChatCompletions) WithStream(stream bool) *ChatCompletions {
        b.request.Stream = common.BoolPtr(stream)
        return b
    }
    
    // Do 执行请求
    func (b *ChatCompletions) Do() (*lkeap.ChatCompletionsResponse, error) {
        return b.client.client.ChatCompletions(b.request)
    
    }
    
    type ChatCompletionsResponse struct {
        Choices []Choice `json:"Choices"`
        Created int64    `json:"Created"`
        Id      string   `json:"Id"`
        Model   string   `json:"Model"`
        Object  string   `json:"Object"`
        Usage   Usage    `json:"Usage"`
    }
    
    type Choice struct {
        Delta Delta `json:"Delta"`
        Index int   `json:"Index"`
    }
    
    type Delta struct {
        Content          string `json:"Content,omitempty"`
        ReasoningContent string `json:"ReasoningContent,omitempty"`
        Role             string `json:"Role,omitempty"`
    }
    
    type Usage struct {
        CompletionTokens int `json:"CompletionTokens"`
    
        PromptTokens     int `json:"PromptTokens"`
        TotalTokens      int `json:"TotalTokens"`
    }
  • 腾讯云大模型知识引擎的实时文档解析 API 封装

    go 复制代码
    package tclkep
    
    import (
        "context"
    
        "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
        lkeap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lkeap/v20240522"
    )
    
    type DocumentParser struct {
        client  *Client
        request *lkeap.ReconstructDocumentSSERequest
    }
    
    func (b *DocumentParser) WithFileType(fileType string) *DocumentParser {
        b.request.FileType = common.StringPtr(fileType)
        return b
    }
    
    func (b *DocumentParser) WithFileUrl(fileUrl string) *DocumentParser {
        b.request.FileUrl = common.StringPtr(fileUrl)
        return b
    }
    
    func (b *DocumentParser) WithFileBase64(fileBase64 string) *DocumentParser {
        b.request.FileBase64 = common.StringPtr(fileBase64)
        return b
    }
    
    // Do 执行请求
    func (b *DocumentParser) Do(ctx context.Context) (*lkeap.ReconstructDocumentSSEResponse, error) {
        return b.client.client.ReconstructDocumentSSEWithContext(ctx, b.request)
    }

    以上封装的目的是为了方便实现链式调用,使代码更加简洁、易读,并提高开发效率,例如:

  • DeepSeek 进行对话:

    go 复制代码
    resp, err := s.client.NewChatCompletions("deepseek-r1").
            WithStream(true).
            WithMessages(messages).
            Do()
  • 实时文档解析:

    go 复制代码
    documentSSEResponse, err := s.client.NewDocumentParser().
            WithFileType(strings.ToUpper(media.DocType)).
            WithFileBase64(base64.StdEncoding.EncodeToString(b)).Do(ctx)

网页内容解析与转换

通过结合 Go 语言的 go-readabilityhtml-to-markdown 两个库,我们可以高效地实现网页内容的解析与格式转换。核心代码实现如下所示:

go 复制代码
var article readability.Article
// 读取网页内容
article, err = readability.FromURL(url, 30*time.Second)
if err != nil {
	return promptInfo, fmt.Errorf("failed to readability.FromURL: %w", err)
}
// 将 html 转成 markdown
content, err = htmltomarkdown.ConvertString(article.Content)
if err != nil {
	return promptInfo, fmt.Errorf("failed to htmltomarkdown.ConvertString: %w", err)
}

将网页内容转换为 markdown 格式是因为 AI 大语言模型在处理 结构化文字(如 Markdown)时表现更好

PDF 文件解析与转换

对于 PDF 文件,首先需要上传到服务器得到文件存储的路径,然后在对话时传递给对话接口,接下来通过路径读取文件内容,然后通过 腾讯云知识引擎原子能力 提供的 实时文档解析 APIPDF 的内容转换成 markdown 格式,以便大模型更易于理解文档内容。

核心代码实现如下所示:

go 复制代码
func (s *ChatService) toMarkdown(ctx context.Context, media domain.Media) (string, string, error) {

	var content string

	filePath := strings.Replace(media.Path, "/static/", "./static/files/", 1)
	// 读取 PDF 文件内容
	b, fileErr := os.ReadFile(filePath)
	if fileErr != nil {
		return "", "", fmt.Errorf("failed to os.ReadFile: %w", fileErr)
	}

	// 调用腾讯云文档解析服务
	documentSSEResponse, err := s.client.NewDocumentParser().
		WithFileType(strings.ToUpper(media.DocType)).
		WithFileBase64(base64.StdEncoding.EncodeToString(b)).Do(ctx)

	if err != nil {

		return "", "", fmt.Errorf("failed to s.client.NewDocumentParser.Do: %w", err)

	}
	// 流式响应
	var documentRecognizeResultUrl string

	for event := range documentSSEResponse.Events {
		type EventResponse struct {
			ProgressMessage            string `json:"ProgressMessage"`
			DocumentRecognizeResultUrl string `json:"DocumentRecognizeResultUrl"`
			StatusCode                 string `json:"StatusCode"`
		}
		eResp := EventResponse{}
		err = json.Unmarshal(event.Data, &eResp)
		if err != nil {
			panic(err)
		}

		if eResp.ProgressMessage == "完成文档解析" && eResp.StatusCode == "Success" {
			documentRecognizeResultUrl = eResp.DocumentRecognizeResultUrl
			break
		}
	}
	// 下载文件
	get, hErr := http.Get(documentRecognizeResultUrl)
	if hErr != nil {
		return "", "", fmt.Errorf("failed to http.Get: %w", hErr)
	}
	defer get.Body.Close()
	// 2. 读取 ZIP 文件内容
	body, err := io.ReadAll(get.Body)

	if err != nil {
		return "", "", err
	}

	// 3. 解压 ZIP 文件
	reader := bytes.NewReader(body)
	zipReader, err := zip.NewReader(reader, int64(len(body)))
	if err != nil {
		return "", "", err
	}

	// 4. 遍历 ZIP 文件中的文件
	for _, file := range zipReader.File {
		// 检查文件扩展名是否为 .md
		if strings.HasSuffix(file.Name, ".md") {
			// 5. 打开文件
			fileReader, err := file.Open()
			if err != nil {
				return "", "", err
			}
			defer fileReader.Close()

			// 6. 读取文件内容

			md, err := io.ReadAll(fileReader)

			if err != nil {
				return "", "", err
			}

			content = string(md)
			break
		}
	}
	return media.FileName, content, nil
}

代码解释:

  • 读取 PDF 文件

    • 解析 media.Path 生成本地文件路径 filePath

    • 使用 os.ReadFile(filePath) 读取 PDF 文件内容,并进行错误处理。

  • 调用腾讯云文档解析 API

    • 通过 s.client.NewDocumentParser() 调用 API,发送 PDF 数据(Base64 编码)。

    • 监听流式响应:

      • 遍历 documentSSEResponse.Events,等待解析完成的事件。

      • 提取 DocumentRecognizeResultUrl(解析结果的下载链接)。

  • 下载解析结果(ZIP 文件)

    • 通过 http.Get(documentRecognizeResultUrl) 下载 ZIP 文件,并读取内容。

    • 使用 io.ReadAll(get.Body) 解析 ZIP 数据。

  • 解压 ZIP 并提取 Markdown 文件

    • 通过 zip.NewReader() 解析 ZIP 文件内容。

    • 遍历 zipReader.File 查找 .md 文件。

    • 读取 Markdown 文件内容,并存入 content 变量。

  • 返回结果

    • 返回文件名 media.FileName 和解析出的 Markdown 内容 content

    • 处理可能的错误(文件读取、API 调用、ZIP 解压等)。

system prompt 与 user prompt 的封装

AI 智能阅读助 里,需要封装两种类型的 promptsystem promptuser prompt

  • system prompt

    • 由开发者或系统设定的隐藏指令,用于定义 AI 的角色、行为准则或回答风格。

    • 在和腾讯云 DeepSeek 第一次对话时,需要携带 system prompt参数,以便让 AI 提供更为精确和定制化的回答。

    • system prompt 参考内容如下:

      你是一位专业的文档助手,负责为用户提供关于文档的详细、准确和清晰的回答。你的任务是帮助用户理解、分析和解决与文档相关的任何问题。

      任务要求:

      1. 回答问题 :针对用户提出的问题,提供详细、准确和易于理解的回答。

      2. 保持专业性 :在回答问题时,始终保持专业、礼貌和客观的态度。

      回答要求:

      1. 简洁明了 :回答应简洁明了,避免冗长和不必要的细节。

      2. 结构化 :使用段落、列表或标题来组织信息,确保易于阅读和理解。

      3. 引用文档 :在回答中引用文档中的具体部分,以支持你的观点或解释。

      4. 提供上下文 :如果问题涉及文档的特定部分,提供足够的上下文信息,帮助用户理解。

      注意事项:

      1. 准确性 :确保所有回答都基于文档中的实际内容,避免猜测或假设。

      2. 用户友好 :使用用户易于理解且与文档关联语言,避免使用超纲或复杂的术语。

  • user prompt:用户直接输入的请求或问题,明确告知 AI 需要执行的任务。

    • 用户选择 阅读当前网页阅读PDF文件 时,第一次对话的 user prompt 由后端系统封装,格式如下:

      问题描述

      帮我总结文档的内容。

      文档

      ${document content}

在之后的对话里,user prompt 会被直接指定为 用户输入 的内容。

小结

在这个 AI 智能阅读助手项目中,我使用了腾讯云提供的 DeepSeek API,充分利用了 DeepSeek 大语言模型的自然语言处理能力。这个模型在理解文本内容、提取关键信息方面表现得非常出色,成为了整个项目的核心技术支撑。腾讯云的 API 服务在稳定性和响应速度上也满足了项目需求,接口调用体验非常流畅,价格方面也是非常实惠。

除了腾讯云的DeepSeek API,我还借助了腾讯云大模型知识引擎的 实时文档解析 APIPDF 的内容转换成 markdown 格式,以便大模型更易于理解文档内容。腾讯云大模型知识引擎除了 DeepSeek 和文档解析 API 以外,还提供了很多与 AI 应用相关的 API,例如 获取特征向量多轮改写 等。

尽管这个阅读助手目前仍处于原型阶段,功能还有待完善,但通过这次开发实践,我对如何将 AI 能力与实际阅读需求相结合有了更深入的思考。这篇文章的主要目的是记录设计思路和技术选择,希望能为有类似需求的开发者提供一些参考和启发。


你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。

我专注于分享 Go 语言相关的技术知识,同时也会深入探讨 AI 领域的前沿技术。

成功的路上并不拥挤,有没有兴趣结个伴?

相关推荐
Asthenia041211 分钟前
面试问题分析:为什么Java能实现反射机制,其他语言不行?
后端
千亿的星空21 分钟前
部队仓储信息化手段建设:基于RFID、IWMS、RCS三大技术的仓储物流全链路效能优化方案
大数据·人工智能·信息可视化·信息与通信·数据库开发·可信计算技术
拳布离手27 分钟前
fastgpt工作流探索
后端
猫先生Mr.Mao30 分钟前
2025年2月AGI技术月评|重构创作边界:从视频生成革命到多模态生态的全面爆发
人工智能·大模型·aigc·agi·多模态·行业洞察
Asthenia041231 分钟前
IO 多路复用详解:从概念->系统调用-> Java 在NIO中实现
后端
Asthenia041236 分钟前
场景题-Java 单体项目优化:应对高并发客户端访问的性能与线程安全分析
后端
安然无虞37 分钟前
31天Python入门——第5天:循环那些事儿
开发语言·后端·python
卧式纯绿38 分钟前
目标检测20年(一)
人工智能·yolo·目标检测·机器学习·计算机视觉·目标跟踪
uhakadotcom43 分钟前
商业智能最好的开源产品和商业产品分别是什么?
后端·面试·github
车载诊断技术1 小时前
电子电气架构 --- 汽车面对软件怎么“破局“?
数据库·人工智能·架构·汽车·电子电器框架·汽车面对软件怎么破局·智能电动汽车概述