1.什么是 Agent?

在 Eino 框架中,Agent(智能体) 代表一个独立且可执行的智能任务单元。它可以被视为一个具备理解指令、执行任务并反馈结果能力的实体。每个 Agent 都拥有明确的名称和描述,这使得它们能够被系统中的其他组件发现并调用。

核心理念:任何需要与大语言模型(LLM)进行复杂交互的场景,都可以被抽象为一个 Agent。

典型的应用场景包括:

信息查询 Agent:实时获取并处理天气、股市等数据。

事务处理 Agent:协助用户预定会议室、安排日程。

领域专家 Agent:基于特定知识库回答专业问题(如法律咨询、医疗问答)。

在 Eino 的架构设计中,Agent 被定义为一个标准的 interface,这为开发者提供了高度的灵活性和扩展性。

本文将基于 Eino 框架的 ADK (Agent Development Kit) 和豆包模型,通过 ChatModelAgent 实现一个具备 PDF 简历解析与人岗匹配分析功能的智能体。

An image to describe post

参考文档
Eino ADK: 概述 | CloudWeGo
Eino ADK: ChatModelAgent | CloudWeGo
Parser - pdf | CloudWeGo

2.实现思路与流程

为了构建这个简历解析 Agent,我们将开发过程拆解为以下五个核心步骤:

  1. 环境配置与模型初始化:创建一个支持工具调用(Function Calling)的 ChatModel
  2. Agent 骨架搭建:使用 Eino ADK 创建 Agent 实例,并注入模型。
  3. Prompt 工程:编写精准的系统指令(System Prompt),指导 AI 如何读取文件、解析内容以及规范输出格式。
  4. 工具开发:实现 pdf_to_text 工具,赋予 Agent 读取本地 PDF 文件的能力。
  5. 业务串联与执行:构建运行环境(Runner),处理用户输入并展示最终的分析结果。
    完整代码可以在 GitHub 查看

3.代码实现

3.1 创建 ChatModel

首先,我们需要配置与大语言模型交互的基础组件。在项目根目录下创建 .env 文件,填入必要的 API 配置:

OPENAI_API_KEY = 你的API key
OPENAI_MODEL_NAME = 模型名称
OPENAI_BASE_URL = 模型api url

Eino 提供了 ToolCallingChatModel 接口,它在基础 BaseChatModel 之上扩展了工具绑定能力(WithTools)。以下代码负责从环境变量加载配置并初始化模型:

// agent.go
func newChatModelFromEnv(ctx context.Context) (model.ToolCallingChatModel, error) {
    err := godotenv.Load()
    if err != nil {
        return nil, fmt.Errorf("failed to load env file: %w", err)
    }

    key := os.Getenv("OPENAI_API_KEY")
    modelName := os.Getenv("OPENAI_MODEL_NAME")
    baseUrl := os.Getenv("OPENAI_BASE_URL")

    chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        BaseURL: baseUrl,
        Model:   modelName,
        APIKey:  key,
    })
    if err != nil {
        return nil, errors.Errorf("Failed to create OpenAI chat model: %v", err)

    }
    return chatModel, err
}

3.2 创建一个 Agent 用于解析 PDF 简历

接下来,使用 adk.NewChatModelAgent 构建 Agent 主体。我们需要配置 Agent 的身份描述、指令(Instruction)以及它所能使用的工具。

// agent.go

func NewResumeAgent() adk.Agent {
    ctx := context.Background()
    chatModel, err := newChatModelFromEnv(ctx)
    if err != nil {
        log.Fatalf("create chat model failed: %v", err)
    }
    baseAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Name:        "ResumeParserAgent",
        Description: "一个专业的简历解析智能体,用于提取简历中的关键信息",
        // 调用 ChatModel 时的 System Prompt,支持 f-string 渲染
        Instruction: "TODO: 提示词",
        // 运行所使用的 ChatModel,要求支持工具调用
        Model: chatModel,
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                Tools: []componentstool.BaseTool{
                    // 此处将注册 PDF 解析工具,详见后文
                },
            },
        },
    })
    
    if err != nil {
        log.Fatal(fmt.Errorf("failed to create resume parser agent: %w", err))
    }
    return baseAgent
}

3.3 设计 System Prompt (提示词)

Prompt 是 Agent 的灵魂。我们需要明确告诉 AI:它的角色是什么、可以使用什么工具、以及必须输出什么格式的数据。

为了确保程序能够稳定解析结果,我们要求 AI 严格输出 JSON 格式。

# Role
你是一位拥有 10 年经验的资深技术招聘专家(Senior Technical Recruiter)。你擅长从简历中提取关键信息,并能结合职位描述(JD)对候选人进行深度画像分析。

# 你可以使用的工具
pdf_to_text: 解析PDF简历

# 注意事项
- 你必须使用提供给你的工具
- 不要跳过工具调用,不要返回空数据
- 必须从简历内容中提取真实的信息
- 只返回JSON格式,不要返回其他文本

# 任务步骤
请阅读用户提供的【候选人简历】和【目标岗位描述(JD)】,完成以下两项任务:
1. 使用工具解析提供的简历文件路径,获取简历的完整文本内容
2. 精准提取简历中的结构化信息。
3. 基于简历内容和 JD,对候选人进行深度评估和人岗匹配分析。

# Analysis Guidelines (分析指南)
在生成 ai_analysis 字段时,请严格遵循以下思维逻辑:

1. **技术栈匹配分析**   - 对比简历技能与 JD 要求的 "Must-have" 和 "Nice-to-have"。
   - 关注技术版本的时效性(如:候选人精通 Java 8,但 JD 需要 Go)。

2. **行业背景与业务理解**   - 分析候选人过往公司的业务领域(如:金融、电商、SaaS)。
   - 判断候选人处理的业务复杂度(如:高并发、海量数据、复杂算法)。

3. **职业发展轨迹 (Career Trajectory)**   - **成长性**:观察职位变迁,判断是否有晋升或职责扩大。
   - **稳定性**:计算平均在职时间,识别是否存在频繁跳槽(<1年或长时间空窗>3个月)。
   - **平台背书**:识别过往公司是否为知名企业或行业头部。

4. **综合素质判断**   - 从项目描述中寻找“解决复杂问题”的能力。
   - 从自我评价、博客、开源项目中判断“学习能力”和“技术热情”。

# Extraction Rules (提取规则)
- **缺失处理**:如果简历中没有明确提到的字段,请填空字符串,严禁编造。
- **时间格式**:统一标准化为 YYYY-MM。
- **薪资标准化**:保留原始描述,如 "20k*14" 或 "30-50万"。
- **项目经历**:重点提取项目中的 tech_stack 和 role。

# Output Format (输出格式)
请直接输出标准的 JSON 格式,不要包含 Markdown 代码块标记(json),也不要包含任何解释性文字。  

JSON 结构模板如下:  
{
    "basic_info": {
        "name": "从简历中提取的真实姓名",
        "work_years": "工作年限(如2.5年,3年,应届毕业生等)",
        "phone": "联系方式(手机)",
        "email": "电子邮箱",
        "city": "现居住城市",
        "job_intention": "求职意向职位",
        "salary_expectation": "期望薪资字符串(如10k, 9000, 15k-25k等)"
    },
    "basic_info_extended": {
        "current_status": "在职/离职/在校",
        "availability": "预计到岗时间 (如: 一周内/一个月)",
        "birth":"出生年月",
        "age": "根据出生年月/教育经历推算的年龄 (可选,作为参考)",
        "gender": "性别 (如果简历中有写)"
    },
    "social_links": {
        "linkedin": "LinkedIn个人主页链接",
        "github": "GitHub个人主页链接",
        "blog": "个人博客链接",
        "stackoverflow": "StackOverflow个人主页链接",
        "其他社交平台链接": "对应的个人主页链接"
    },
    "education": [
        {
            "school": "学校名称",
            "degree": "学历",
            "major": "专业",
            "start_year": "入学年份",
            "end_year": "毕业年份",
            "gpa":"绩点(如 3.8/4.0 或 Top 5%)",
            "major_courses":["主修课程","数据结构","编译原理"]
        }
    ],
    "work_experience": [
        {
            "company": "公司名称",
            "position": "职位名称",
            "start_date": "入职时间(格式如:2018-03)",
            "end_date": "离职时间(格式如:2019-08或至今)",
            "description": "工作职责描述",
            "achievements": "工作成就描述",
            "tech_stack": "公司项目用到的技术栈"
        }
    ],
    "projects": [
        {
            "name": "从简历中提取的项目名称",
            "role": "从简历中提取的在项目中的角色",
            "start_date": "从简历中提取的项目开始时间(格式如:2018-03)",
            "end_date": "从简历中提取的项目结束时间(格式如:2019-08或至今)",
            "description": "项目背景与描述",
            "tech_stack": "项目用到的技术栈",
            "link": "项目链接/demo地址(如果没有,就为空)"
        }
    ],
    "skills": [
        "从简历中提取的技能1",
        "从简历中提取的技能2"
    ],
    "certifications": [
        "技能证书1",
        "技能证书2"
    ],
    "languages": [
        {
            "language": "语种(如:英语)",
            "proficiency": "熟练程度(如:熟练,一般,精通,CET-6, 雅思7.0等)"
        }
    ],
    "awards": [
        {
            "name": "奖项名称",
            "date": "获奖时间(格式如:2019-05)",
            "level": "奖项级别(如:校级,省级,国家级,国际级等)"
        }
    ],
    "ai_analysis": {
        "summary": "AI生成的一段200字以内的候选人画像总结, 方便面试官快速了解",
        "highlights": [
            "候选人亮点",
            "拥有5年高并发系统设计经验,与岗位需求高度匹配",
            "具有PMP证书,具备良好的项目管理和团队协作能力",
            "有从0到1搭建SaaS平台的完整经历"
        ],
        
        "job_match_gaps": [
            "岗位匹配缺口",
            "结合JD 分析候选人与岗位的匹配度不足之处",
            "缺少岗位要求的Go语言实战经验,主要技术栈为Java",
            "未涉及过海外支付业务,业务背景与JD有一定偏差",
            "目前居住地在上海,岗位在北京,需要确认异地入职意愿"
        ],
        "match_tags": [
            "自动提取候选人标签",
            "团队管理",
            "金融行业背景"
        ],
        "risk_flags": [
            "风险提示",
            "频繁跳槽 (平均在职时间小于1年)",
            "存在6个月以上的空窗期"
        ],
        "leadership_potential": "根据过往经历判断其带团队的潜力 (高/中/低)",
        "suggested_interview_direction": [
            "Agent 根据简历内容自动生成的个性化面试题的提问方向",
            "方向1-技术面: 我看到你在XX项目中使用了Redis, 请问你是如何解决缓存穿透问题的,",
            "方向2-HR面: 你的简历中有两段经历时间重叠, 可以解释一下吗?"
        ]
    }
}

目前 agent.go 的代码如下:

package resume_agent

import (
    "context"
    tool2 "demo/toolx"
    "fmt"
    "github.com/cloudwego/eino-ext/components/model/openai"
    "github.com/cloudwego/eino/adk"
    "github.com/cloudwego/eino/components/model"
    componentstool "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/compose"
    "github.com/joho/godotenv"
    "github.com/pkg/errors"
    "log"
    "os"
)

func NewResumeAgent() adk.Agent {
    ctx := context.Background()
    chatModel, err := newChatModelFromEnv(ctx)
    if err != nil {
        log.Fatalf("create chat model failed: %v", err)
    }
    baseAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Name:        "ResumeParserAgent",
        Description: "一个专业的简历解析智能体,用于提取简历中的关键信息",
        // 调用 ChatModel 时的 System Prompt,支持 f-string 渲染
        Instruction: genInstructionPrompt(),
        // 运行所使用的 ChatModel,要求支持工具调用
        Model: chatModel,
        //可以通过 ToolsConfig 为 ChatModelAgent 配置 Tool
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                Tools: []componentstool.BaseTool{
                    // 【TODO】:还需要给Agent准备PDF工具
                },
            },
        },
        // react 模式下 ChatModel 最大生成次数,超过时 Agent 会报错退出,默认值为 20
        MaxIterations: 20,
    })
    if err != nil {
        log.Fatal(fmt.Errorf("failed to create resume parser agent: %w", err))
    }
    return baseAgent
}
func newChatModelFromEnv(ctx context.Context) (model.ToolCallingChatModel, error) {
    err := godotenv.Load()
    if err != nil {
        return nil, fmt.Errorf("failed to load env file: %w", err)
    }

    key := os.Getenv("OPENAI_API_KEY")
    modelName := os.Getenv("OPENAI_MODEL_NAME")
    baseUrl := os.Getenv("OPENAI_BASE_URL")

    chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        BaseURL: baseUrl,
        Model:   modelName,
        APIKey:  key,
    })
    if err != nil {
        return nil, errors.Errorf("Failed to create OpenAI chat model: %v", err)

    }
    return chatModel, err
}

func genInstructionPrompt() string {
    return `
# Role
你是一位拥有 10 年经验的资深技术招聘专家(Senior Technical Recruiter)。你擅长从简历中提取关键信息,并能结合职位描述(JD)对候选人进行深度画像分析。

# 你可以使用的工具
pdf_to_text: 解析PDF简历

# 注意事项
- 你必须使用提供给你的工具
- 不要跳过工具调用,不要返回空数据
- 必须从简历内容中提取真实的信息
- 只返回JSON格式,不要返回其他文本

# 任务步骤
请阅读用户提供的【候选人简历】和【目标岗位描述(JD)】,完成以下两项任务:
1. 使用工具解析提供的简历文件路径,获取简历的完整文本内容
2. 精准提取简历中的结构化信息。
3. 基于简历内容和 JD,对候选人进行深度评估和人岗匹配分析。

# Analysis Guidelines (分析指南)
在生成 ai_analysis 字段时,请严格遵循以下思维逻辑:

1. **技术栈匹配分析**:
   - 对比简历技能与 JD 要求的 "Must-have" 和 "Nice-to-have"。
   - 关注技术版本的时效性(如:候选人精通 Java 8,但 JD 需要 Go)。

2. **行业背景与业务理解**:
   - 分析候选人过往公司的业务领域(如:金融、电商、SaaS)。
   - 判断候选人处理的业务复杂度(如:高并发、海量数据、复杂算法)。

3. **职业发展轨迹 (Career Trajectory)**:
   - **成长性**:观察职位变迁,判断是否有晋升或职责扩大。
   - **稳定性**:计算平均在职时间,识别是否存在频繁跳槽(<1年)或长时间空窗(>3个月)。
   - **平台背书**:识别过往公司是否为知名企业或行业头部。

4. **综合素质判断**:
   - 从项目描述中寻找“解决复杂问题”的能力。
   - 从自我评价、博客、开源项目中判断“学习能力”和“技术热情”。

# Extraction Rules (提取规则)
- **缺失处理**:如果简历中没有明确提到的字段,请填空字符串,严禁编造。
- **时间格式**:统一标准化为 YYYY-MM。
- **薪资标准化**:保留原始描述,如 "20k*14" 或 "30-50万"。
- **项目经历**:重点提取项目中的 tech_stack 和 role。

# Output Format (输出格式)
请直接输出标准的 JSON 格式,不要包含 Markdown 代码块标记(json),也不要包含任何解释性文字。  

JSON 结构模板如下:  


{
    "basic_info": {
        "name": "从简历中提取的真实姓名",
        "work_years": "工作年限(如2.5年,3年,应届毕业生等)",
        "phone": "联系方式(手机)",
        "email": "电子邮箱",
        "city": "现居住城市",
        "job_intention": "求职意向职位",
        "salary_expectation": "期望薪资字符串(如10k, 9000, 15k-25k等)"
    },
    "basic_info_extended": {
        "current_status": "在职/离职/在校",
        "availability": "预计到岗时间 (如: 一周内/一个月)",
        "birth":"出生年月",
        "age": "根据出生年月/教育经历推算的年龄 (可选,作为参考)",
        "gender": "性别 (如果简历中有写)"
    },
    "social_links": {
        "linkedin": "LinkedIn个人主页链接",
        "github": "GitHub个人主页链接",
        "blog": "个人博客链接",
        "stackoverflow": "StackOverflow个人主页链接",
        "其他社交平台链接": "对应的个人主页链接"
    },
    "education": [
        {
            "school": "学校名称",
            "degree": "学历",
            "major": "专业",
            "start_year": "入学年份",
            "end_year": "毕业年份",
            "gpa":"绩点(如 3.8/4.0 或 Top 5%)",
            "major_courses":["主修课程","数据结构","编译原理"]
        }
    ],
    "work_experience": [
        {
            "company": "公司名称",
            "position": "职位名称",
            "start_date": "入职时间(格式如:2018-03)",
            "end_date": "离职时间(格式如:2019-08或至今)",
            "description": "工作职责描述",
            "achievements": "工作成就描述",
            "tech_stack": "公司项目用到的技术栈"
        }
    ],
    "projects": [
        {
            "name": "从简历中提取的项目名称",
            "role": "从简历中提取的在项目中的角色",
            "start_date": "从简历中提取的项目开始时间(格式如:2018-03)",
            "end_date": "从简历中提取的项目结束时间(格式如:2019-08或至今)",
            "description": "项目背景与描述",
            "tech_stack": "项目用到的技术栈",
            "link": "项目链接/demo地址(如果没有,就为空)"
        }
    ],
    "skills": [
        "从简历中提取的技能1",
        "从简历中提取的技能2"
    ],
    "certifications": [
        "技能证书1",
        "技能证书2"
    ],
    "languages": [
        {
            "language": "语种(如:英语)",
            "proficiency": "熟练程度(如:熟练,一般,精通,CET-6, 雅思7.0等)"
        }
    ],
    "awards": [
        {
            "name": "奖项名称",
            "date": "获奖时间(格式如:2019-05)",
            "level": "奖项级别(如:校级,省级,国家级,国际级等)"
        }
    ],
    "ai_analysis": {
        "summary": "AI生成的一段200字以内的候选人画像总结, 方便面试官快速了解",
        "highlights": [
            "候选人亮点",
            "拥有5年高并发系统设计经验,与岗位需求高度匹配",
            "具有PMP证书,具备良好的项目管理和团队协作能力",
            "有从0到1搭建SaaS平台的完整经历"
        ],
        
        "job_match_gaps": [
            "岗位匹配缺口",
            "结合JD 分析候选人与岗位的匹配度不足之处",
            "缺少岗位要求的Go语言实战经验,主要技术栈为Java",
            "未涉及过海外支付业务,业务背景与JD有一定偏差",
            "目前居住地在上海,岗位在北京,需要确认异地入职意愿"
        ],
        "match_tags": [
            "自动提取候选人标签",
            "团队管理",
            "金融行业背景"
        ],
        "risk_flags": [
            "风险提示",
            "频繁跳槽 (平均在职时间小于1年)",
            "存在6个月以上的空窗期"
        ],
        "leadership_potential": "根据过往经历判断其带团队的潜力 (高/中/低)",
        "suggested_interview_direction": [
            "Agent 根据简历内容自动生成的个性化面试题的提问方向",
            "方向1-技术面: 我看到你在XX项目中使用了Redis, 请问你是如何解决缓存穿透问题的,",
            "方向2-HR面: 你的简历中有两段经历时间重叠, 可以解释一下吗?"
        ]
    }
}
`
}

3.4 实现 PDF 解析工具

Agent 自身无法直接“看”到文件,需要通过工具(Tool)来赋予其能力。Eino 的 ToolsNode 支持 InvokableTool(同步调用工具)。

我们将封装 Eino 生态中的 pdfParser 组件,将其包装为一个 Agent 可调用的工具。

ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                Tools: []componentstool.BaseTool{
                    // pdf_to_text是prompt中写好的工具名称, 大模型根据这个来调用PDF工具
                    NewPdfToolWithEino("pdf_to_text")
                },
            },
        },

具体实现:

package pdf

import (
    "context"
    "fmt"
    "os"
    "time"
    pdfParser "github.com/cloudwego/eino-ext/components/document/parser/pdf"
    "github.com/cloudwego/eino/components/document/parser"
    "github.com/cloudwego/eino/components/tool"
    einoutils "github.com/cloudwego/eino/components/tool/utils"
    "log"
    "strings"
)

func NewPdfToolWithEino(name string) tool.InvokableTool {
    // 用于告诉模型如何/何时/为什么使用这个工具
    // 可以在描述中包含少量示例
    toolDesc := `
    将本地PDF转换为纯文本
    需传入本地PDF的绝对路径
`
    pdfTool, err := einoutils.InferTool(name, toolDesc, convertPdfToText)
    if err != nil {
        log.Fatalf("NewPdfToolWithEino failed, err: %v", err)
    }
    log.Println("使用eino提供的 pdf 解析器, 初始化完成, 工具名称: ", name)

    return pdfTool
}

func convertPdfToText(ctx context.Context, req *ParsePdfRequest) (*ParsePdfResponse, error) {
    result := &ParsePdfResponse{
        Success: false,
        Meta:    genMeta(req),
    }

    file, err := validateAndOpenPdf(req)
    if err != nil {
        result.ErrorMsg = err.Error()
        return result, nil
    }
    defer file.Close()

    // 按大模型传入的参数决定是否分页
    einoPdfParser, err := pdfParser.NewPDFParser(ctx, &pdfParser.Config{ToPages: req.ToPages})
    if err != nil {
        result.ErrorMsg = fmt.Sprintf("初始化eino pdf解析器失败: %v", err)
        return result, nil
    }

    docs, err := einoPdfParser.Parse(ctx, file,
        parser.WithURI(req.FilePath),
        parser.WithExtraMeta(result.Meta),
    )

    if err != nil {
        result.ErrorMsg = fmt.Sprintf("eino pdf解析器解析文件失败: %v", err)
        return result, nil
    }

    result.Success = true
    result.TotalPages = len(docs)

    if req.ToPages {
        // 分页
        // 分页模式:按页码整理文本(用索引+1作为页码,可靠无依赖)
        pages := make([]PdfPageText, 0, len(docs))
        for idx, doc := range docs {
            pages = append(pages, PdfPageText{
                Page:    idx + 1,
                Content: doc.Content,
            })
        }
        result.Pages = pages
    } else {
        // 不分页
        var sb strings.Builder
        for _, doc := range docs {
            sb.WriteString(doc.Content)
            sb.WriteString("\n")
        }
        result.Content = sb.String()
    }

    return result, nil
}



type ParsePdfRequest struct {
    FilePath string `json:"filePath" jsonschema:"description=本地PDF文件的绝对路径"`
    ToPages  bool   `json:"toPages" jsonschema:"description=是否按页分割文本,true=分页输出"`
}

type ParsePdfResponse struct {
    Success    bool          `json:"success" jsonschema:"description=是否解析成功"`
    Content    string        `json:"content" jsonschema:"description=解析出的文本内容"`
    Pages      []PdfPageText `json:"pages" jsonschema:"description=按页分割的文本内容(仅当toPages为true时返回)"`
    TotalPages int           `json:"totalPages" jsonschema:"description=总页数"`
    ErrorMsg   string        `json:"errorMsg,omitempty" jsonschema:"description=解析失败时的错误信息"`
    Meta       ParsePdfMeta  `json:"meta,omitempty" jsonschema:"description=解析时的元数据"`
}

type ParsePdfMeta = map[string]any

type PdfPageText struct {
    Page    int    `json:"page" jsonschema:"description=页码"`
    Content string `json:"content" jsonschema:"description=该页的文本内容"`
}


func genMeta(req *ParsePdfRequest) ParsePdfMeta {
    meta := make(ParsePdfMeta)
    meta["filePath"] = req.FilePath
    meta["toPages"] = req.ToPages
    meta["parseTime"] = time.Now().Format("2006-01-02 15:04:05")
    return meta
}

// 校验并打开PDF文件
func validateAndOpenPdf(req *ParsePdfRequest) (*os.File, error) {
    if req.FilePath == "" {
        return nil, fmt.Errorf("必须传入 pdf 文件的绝对路径")
    }
    file, err := os.Open(req.FilePath)
    if err != nil {
        return nil, fmt.Errorf("打开PDF文件失败:%v(请检查路径是否正确、文件是否存在)", err)
    }
    return file, err
}


3.5 运行 Agent (Main 函数)

最后,通过 adk.Runner 将 Agent 运行起来。我们将传入候选人简历路径和具体的岗位描述(JD),观察 Agent 的分析过程。

package main

import (
    "context"
    "example-eino-pdf-agent/agents/resume"
    "fmt"
    "github.com/cloudwego/eino/adk"
    "github.com/cloudwego/eino/schema"
    "log"
    "time"
)

// 获取岗位描述信息,用于分析岗位匹配度
func getJobDescription() string {
    return `
公司名称: 飞牛
岗位名称: C/C++开发工程师
岗位职责:
负责NAS项目的核心功能开发, 包括数据恢复, 网络配置, 虚拟机管理, docker管理等功能模块的开发。

任职要求:
C++/PostgreSQL/STL/Linux开发/部署经验
1)扎实的C++编程技术, 有C++开发经验, 熟悉常用的数据结构、算法;
2) 熟悉Linux操作系统及Linux多线程、进程通信等内容的编程;熟悉计算机网络编程(TCP/IP、UDP等网络通信协议);
3) 了解Linux文件系统, 有存储类应用开发优先考虑;
4) 有Linux下软RAID使用经验, 了解常用磁盘整列原理(RAID-0, RAID-1, RAID-5等)优先考虑。

公司介绍:
铁刃智造是一家专注于研发安全易用、免费的NAS系统的企业。
我们始终以用户需求为导向,致力于为家庭、工作室等中小企业提供高效、安全、可靠的数据存储和处理解决方案。我们的团队由原UC、PP助手核心成员组成,秉持长期主义的态度,打造出极致体验且安全可靠的NAS系统,为全球市场带来可称之为“国产之光”的产品。
目前主营业务主要有包括:NAS、私有存储周边硬件、私有数据资产+AI解决方案。
`
}

func getUserQuery(userPdf string, jobDesc string) string {
    return fmt.Sprintf(`
【重要】请立即解析以下简历文件并提取关键信息:

简历文件路径:%s
岗位描述(JD): %s

【必须执行的步骤】:
1. 【第一步】立即使用工具解析简历文件,获取完整的简历文本内容
2. 【第二步】从解析的简历文本中提取所有关键信息
3. 【第三步】根据提取的关键信息, 结合 岗位描述(JD) 生成符合要求的 JSON 格式输出

【重要提示】:
- 不要跳过 pdf_to_text 工具调用
- 必须从简历内容中提取真实的信息,不要返回空数据
- 所有JSON字段都必须填充实际内容
- 只返回JSON格式,不要返回其他文本

请返回完整的 JSON 格式结果。
`, userPdf, jobDesc)
}

func main() {
    // PDF文件的绝对路径
    userPdf := "/Users/lucas/work/code/go/example-eino-pdf-agent/examples/test.pdf"

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
    defer cancel()

    agent := resume.NewResumeAgent()
    runner := adk.NewRunner(ctx, adk.RunnerConfig{
        Agent: agent,
        // 用于向 Agent 建议其输出模式,但它并非一个强制性约束。
        // 它的核心思想是控制那些同时支持流式和非流式输出的组件的行为,例如 ChatModel
        // 当 EnableStreaming=false 时,对于那些既能流式也能非流式输出的组件,此时会使用一次性返回完整结果的非流式模式。
        EnableStreaming: false,
    })

    // 执行对话
    input := []adk.Message{
        schema.UserMessage(getUserQuery(userPdf, getJobDescription())),
    }
    events := runner.Run(ctx, input)
    output := ""
    for {
        event, ok := events.Next()
        if !ok {
            break
        }

        if event.Err != nil {
            log.Printf("event错误: %v", event.Err)
            break
        }

        if msg, err := event.Output.MessageOutput.GetMessage(); err == nil {
            output = msg.Content
        }
    }

    log.Println(output)

}

运行效果:

An image to describe post

4.总结

通过上述步骤,我们利用 Eino 框架成功构建了一个具备“感知能力”(通过 Tool 读取文件)和“认知能力”(通过 LLM 分析匹配度)的智能体。

这个示例展示了 Eino ADK 的核心优势:

  • 标准化封装:通过 adk.Agent 接口统一了智能体的定义。
  • 工具编排简便InferTool 让普通的 Go 函数能快速转化为 LLM 可调用的工具。
  • 开发体验流畅:从模型配置到业务运行,链路清晰,扩展性强。

完整代码可以在 GitHub 查看