Eino AI 实战:解析 PDF 文件 & 实现 MCP Server

字数 1632,阅读大约需 9 分钟

前言

大家好,我是码一行。

在 AI 应用开发中,文档解析是一个常见的需求,尤其是 PDF 文档的解析。

Eino 作为 Go 语言编写的 LLM 应用开发框架,提供了强大的文档解析能力。

不知道怎么用的,请看历史文章:

本文会分为两大章节:

  1. 如何解析 PDF
  2. 如何封装成 MCP

详细介绍如何使用 Eino 框架实现 PDF 解析,并基于 Go 官方的 MCP(Model Context Protocol)库将其封装为 MCP 服务,方便本地调用和集成。

第一章:Eino 如何解析 PDF

依赖库如下:

1. 实现原理

Eino 解析 PDF 的核心原理是:

  • 使用 Eino 内置的 PDF 解析组件读取 PDF 文件内容
  • 将解析的文本按照排版结构进行智能切割
  • 识别章节标题和内容分隔特征
  • 将内容组织成结构化的 Document 对象,便于后续处理

2. 实现方案

  1. 依赖选择 :使用 github.com/cloudwego/eino-ext/components/document/parser/pdf 库进行基本 PDF 解析
  2. 排版切割 :实现 splitByLayout 函数,根据章节标题特征进行智能切割
  3. 章节识别:识别常见的中文文档章节标题,如"教育经历"、"个人优势"、"专业技能"等
  4. 结果组织 :将切割后的内容组织成多个 schema.Document 对象,每个对象对应一个章节

3. 代码实现

go 复制代码
package parser

import (
        "context"
        "regexp"
        "strings"

        "os"

        "github.com/cloudwego/eino-ext/components/document/parser/pdf"
        "github.com/cloudwego/eino/components/document/parser"
        "github.com/cloudwego/eino/schema"
)

func ParserPdf(ctx context.Context, path string, options ...parser.Option) ([]*schema.Document, error) {
        parser, _ := pdf.NewPDFParser(ctx, &pdf.Config{
                ToPages: false, // 是否按页面分割文档
        })

        file, err := os.Open(path)
        if err != nil {
                return nil, err
        }

        defer file.Close()

        // 解析文档
        docs, err := parser.Parse(ctx, file, options...)
        if err != nil {
                return nil, err
        }

        // 如果解析结果为空,直接返回
        if len(docs) == 0 {
                return docs, nil
        }

        // 按照排版结构切割文档
        return splitByLayout(docs[0].Content), nil
}

// splitByLayout 按照排版结构切割文本,将内容分割为多个章节
func splitByLayout(content string) []*schema.Document {
        // 定义章节标题列表
        sectionTitles := []string{
                "教育经历", "个人优势", "专业技能", "工作经历", "项目经历",
                "教育背景", "个人简介", "专业能力", "实习经历", "工作经验",
                "项目经验", "获奖情况", "证书", "自我评价",
        }

        // 预处理:在章节标题前添加换行符,便于后续分割
        processedContent := content
        for _, title := range sectionTitles {
                processedContent = strings.ReplaceAll(processedContent, title, "\n"+title+"\n")
        }

        // 定义章节标题正则表达式,匹配可能有换行符的章节标题
        sectionRegex := regexp.MustCompile(`(?m)^\s*(教育经历|个人优势|专业技能|工作经历|项目经历|教育背景|个人简介|专业能力|实习经历|工作经验|项目经验|获奖情况|证书|自我评价)\s*$`)

        // 查找所有章节标题的位置
        matches := sectionRegex.FindAllStringIndex(processedContent, -1)
        if len(matches) == 0 {
                // 如果没有找到章节标题,返回原始内容
                return []*schema.Document{
                        {
                                Content:  content,
                                MetaData: map[string]any{"section": "完整内容"},
                        },
                }
        }

        var result []*schema.Document

        // 处理第一个章节之前的内容
        if matches[0][0] > 0 {
                preContent := strings.TrimSpace(processedContent[:matches[0][0]])
                if preContent != "" {
                        result = append(result, &schema.Document{
                                Content:  preContent,
                                MetaData: map[string]any{"section": "头部信息"},
                        })
                }
        }

        // 处理每个章节
        for i, match := range matches {
                sectionTitle := processedContent[match[0]:match[1]]
                var sectionContent string

                // 确定章节内容的结束位置
                if i < len(matches)-1 {
                        // 不是最后一个章节,结束位置是下一个章节的开始
                        sectionContent = strings.TrimSpace(processedContent[match[1]:matches[i+1][0]])
                } else {
                        // 最后一个章节,结束位置是文本末尾
                        sectionContent = strings.TrimSpace(processedContent[match[1]:])
                }

                // 添加章节到结果中
                result = append(result, &schema.Document{
                        Content:  sectionContent,
                        MetaData: map[string]any{"section": sectionTitle},
                })
        }

        return result
}

第二章:Eino 如何封装 MCP

依赖库如下:

1. 实现原理

Eino 封装 MCP 的核心原理是:

  • 注册 PDF 解析工具到 MCP 服务
  • 处理 MCP 请求,调用相应的工具实现
  • 返回符合 MCP 协议的响应

2. 实现方案

  1. 依赖引入:引入 Go 官方 MCP 库
  2. 服务实现:实现 MCP 服务的核心接口
  3. 工具注册:将 PDF 解析工具注册到 MCP 服务
  4. HTTP 服务:使用 Gin 框架暴露 MCP 服务端点

3. 代码实现

go 复制代码
package parser

import (
        "context"
        "log"
        "net/http"

        "github.com/gin-gonic/gin"
        "github.com/modelcontextprotocol/go-sdk/mcp"
)

func init() {
        serverMCP = mcp.NewServer(&mcp.Implementation{
                Name:    "ParserPDF",
                Version: "1.0.0",
        }, nil)

        // 初始化时注册 ParserPDF 工具
        CreateTool("ParserPDF", "解析 PDF 文件,支持本地路径和 Web URL")
}

func parserPdf(ctx context.Context, req *mcp.CallToolRequest, input McpPdfPrams) (*mcp.CallToolResult, McpPdfResp, error) {
        data, err := ParserPdf(ctx, input.Path)
        if err != nil {
                return nil, McpPdfResp{}, err
        }

        if len(data) == 0 {
                return &mcp.CallToolResult{}, McpPdfResp{Content: ""}, nil
        }

        return &mcp.CallToolResult{}, McpPdfResp{Content: data[0].Content}, nil
}

func CreateTool(name, desc string) {
        mcp.AddTool(serverMCP, &mcp.Tool{
                Name:        name,
                Description: desc,
        }, parserPdf)
}

func Run(ctx context.Context) error {
        // 启动一个新的Goroutine来运行MCP服务器(使用StdioTransport)
        go func() {
                if err := serverMCP.Run(ctx, &mcp.StdioTransport{}); err != nil {
                        log.Printf("MCP服务器运行失败: %v\n", err)
                }
        }()

        // 创建HTTP服务器以支持外部Agent调用
        r := gin.Default()

        // 设置外部调用接口
        r.POST("/api/parser/pdf", func(c *gin.Context) {
                var req struct {
                        Path string `json:"path"`
                }

                if err := c.ShouldBindJSON(&req); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
                        return
                }

                // 直接调用PDF解析函数
                data, err := ParserPdf(c.Request.Context(), req.Path)
                if err != nil {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "PDF解析失败: " + err.Error()})
                        return
                }

                c.JSON(http.StatusOK, gin.H{
                        "status": "success",
                        "data":   data,
                })
        })

        // 提供MCP工具信息接口
        r.GET("/api/mcp/info", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{
                        "name":        "ParserPDF",
                        "version":     "1.0.0",
                        "description": "解析PDF文件,支持本地路径和Web URL",
                })
        })

        // 启动HTTP服务器,监听7777端口
        log.Printf("MCP外部调用服务器启动,监听端口7777")
        return r.Run(":7777")
}

结语

这里并没有 Eino 实现的 MCP Server ,根据最新的官方文档,并没有发现可以实现 MCP Server,只是可以 MCP Client 的方式调用外部或内部的 MCP Server。目前等待官方的最新消息。

通过本文的实践,我们可以看到:

  1. Eino 框架提供了强大的文档解析能力,能够方便地实现 PDF 文档的解析和排版切割。
  2. 基于 Go 官方的 MCP 库,可以快速实现标准化的 MCP 服务,提高服务的兼容性和互操作性。
  3. MCP 协议为 AI 应用提供了标准化的工具调用方式,方便不同组件之间的集成。

这种实现方式具有良好的扩展性和可维护性,可以方便地集成到各种 AI 应用中,满足不同场景下的文档解析需求。

随着 AI 应用的不断发展,基于 MCP 协议的工具调用将在更多场景中得到应用,为 AI 应用提供更强大的能力扩展。

参考文献

相关推荐
Victor3562 小时前
Redis(152) Redis的CPU使用如何监控?
后端
P***84392 小时前
解决Spring Boot中Druid连接池“discard long time none received connection“警告
spring boot·后端·oracle
雨中散步撒哈拉2 小时前
17、做中学 | 初三下期 Golang文件操作
开发语言·后端·golang
倚肆2 小时前
Spring Boot CORS 配置详解:CorsConfigurationSource 全面指南
java·spring boot·后端
databook2 小时前
告别盲人摸象,数据分析的抽样方法总结
后端·python·数据分析
v***44672 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
q***72192 小时前
Spring Boot(快速上手)
java·spring boot·后端
IT_陈寒3 小时前
Redis性能翻倍的5个冷门技巧,90%开发者都不知道第3个!
前端·人工智能·后端
p***97613 小时前
SpringBoot(7)-Swagger
java·spring boot·后端