真实复盘:我是如何用 Go + Eino 在 2 周内上线一个 AI 面试平台的?

89 阅读4分钟

很多同学觉得 AI 开发门槛高,动不动就是微调模型、GPU 算力。其实在 AI Engineering(AI 工程化) 时代,普通后端开发者才是主力军。

本文将复盘我如何利用 字节跳动 Eino 框架,配合 Hertz + Milvus,在 2 周内从零打造并上线一个商业级 AI 面试 Agent 平台的全过程。文末附完整架构图与源码。

一、 需求分析:我们到底要做什么?

接到的需求很简单:做一个 “AI 面试官”。 但作为架构师,脑子里瞬间崩出了无数个工程难题:

  1. 交互模式:是类似 ChatGPT 的纯流式对话,还是像问卷一样的填空?
  2. 上下文管理:面试通常有 10-20 轮,Token 肯定会炸,怎么做长窗口记忆
  3. 幻觉控制:AI 瞎编面试题怎么办?比如问 Java 却出了个 Go 的 GMP 模型。
  4. 延迟优化:大模型生成太慢,用户等不及怎么办?

最终确定的技术方案是:Go (高性能后端) + Eino (Agent 编排) + Milvus (RAG 知识库) + SSE (流式传输)。


二、 架构设计:Eino 带来的降维打击

如果用 Python 写,我可能会选 LangChain。但在 Go 生态里,Eino 是目前的最佳选择。 它把 Agent 的思考过程抽象成了 Graph(图)

  • Node(节点):代表一个动作,比如“查简历”、“生成问题”、“评估回答”。
  • Edge(边):代表流转逻辑,比如“如果回答正确 -> 下一题”,“如果回答模糊 -> 追问”。

2.1 核心编排逻辑(Show Me The Code)

我们定义了一个 NewSocialComprehensiveAgent(社招综合面试官)。 注意看,这里没有乱七八糟的 if-else,只有清晰的组件组装:

// backend/chatApp/agent/interview/comprehensive/social_comprehensive_agent.go

func NewSocialComprehensiveAgent(userId uint, needResumeTool bool) (adk.Agent, error) {
    // 1. 定义大模型底座(这里接入了 DeepSeek/OpenAI)
    model, err := chat.CreatOpenAiChatModel(ctx, userId)
    
    // 2. 挂载简历分析工具
    // 这是 Agent 的“眼睛”,能读取 PDF 内容
    var toolsConfig adk.ToolsConfig
    if needResumeTool {
        toolsConfig = adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                Tools: []componenttool.BaseTool{
                    tool2.GetResumeInfoTool(),
                },
            },
        }
    }

    // 3. 组装 Agent
    baseAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Name:        "SocialInterviewer",
        // System Prompt 是灵魂,定义了面试官的性格
        Instruction: SocialComprehensiveAgentInstruction, 
        Model:       model,
        ToolsConfig: toolsConfig,
        // 允许 Agent 自动思考 15 轮,确保任务完成
        MaxIterations: 15,
    })
    return baseAgent, nil
}

工程化思考:通过 ToolsConfig 的解耦,我们可以随时给面试官增加新能力(比如增加一个“LeetCode 判题工具”),而不需要修改核心对话逻辑。


三、 RAG 实战:让 AI 拥有“题库”

为了解决幻觉,我们建立了一个包含 10w+ 面试题的 Milvus 向量数据库

3.1 混合检索策略

单纯的向量搜索(Vector Search)在精确匹配上很弱。比如我想搜“Java 中级 难度”的题,向量可能会搜出“Go 高级”。 所以我们实现了 Hybrid Search

// backend/internal/eino/milvus/retrieval/retriever.go

// 1. 定义过滤条件
filter := fmt.Sprintf("language == 'Java' && difficulty == 'Medium'")

// 2. 执行检索
// Eino 框架底层封装了 Milvus SDK,调用非常丝滑
docs, err := retriever.Retrieve(ctx, query, 
    retrieval.WithFilter(filter), // 标量过滤
    retrieval.WithTopK(3),        // 只取前三
)

这样,AI 提出的每一个问题,背后都有真实的题库支撑,准确率从 60% 提升到了 99%。


四、 性能优化:如何抗住高并发?

AI 应用最大的瓶颈是 LLM 的推理延迟(首字延迟通常 > 1s)。 为了不阻塞 Go 的协程,我们设计了一套全异步架构

4.1 异步削峰架构图

graph LR
    User[用户前端] --1. 发送回答--> API[Hertz 网关]
    API --2. 写入任务--> Redis[Redis Queue]
    API --3. 返回 TaskID--> User
    
    Worker[后台 Consumer] --4. 抢占任务--> Redis
    Worker --5. 调用 Agent--> Eino[Eino Agent]
    Eino --6. 流式生成--> SSE[SSE 通道]
    SSE --7. 实时推送--> User

代码实现(Redis Queue):

// backend/internal/mq/redis_queue.go

// 生产者:非阻塞写入
func (q *RedisQueue) Publish(ctx context.Context, topic string, msg []byte) error {
    return q.client.LPush(ctx, topic, msg).Err()
}

// 消费者:阻塞读取,节省 CPU
func (c *Consumer) Start() {
    for {
        res, _ := c.client.BRPop(ctx, 0, c.topic).Result()
        go c.handleTask(res[1]) // 并发处理任务
    }
}

通过这套架构,单机 QPS 提升了 10 倍 以上,且用户体验极其丝滑,就像在看真实的面试官打字一样。


五、 总结与资源

这 2 周的实战让我明白:AI 只是能力,工程化才是落地。 作为 Go 开发者,我们不需要去卷模型算法,只要利用好 Eino 这种优秀的编排框架,结合我们擅长的 高并发、微服务 能力,就能在 AI 时代占有一席之地。

image.png

如果你对这个项目的完整源码(含后端 Go、前端 Next.js、部署脚本)感兴趣,或者想深入学习 Eino 框架

👉 资源获取: 关注掘金专栏,或关注公众号【王中阳】,回复“面试吧”,即可免费获取项目架构图和部分核心源码。

私信备注“掘金”,拉你进 Eino 技术交流群,和字节大佬一起交流。