Featured image of post AI面试官 —— 打造你的专属加薪助手

AI面试官 —— 打造你的专属加薪助手

AI 多轮对话引子

之前分享过几个AI应用案例:个人兴趣助手、塔罗牌占卜、AI新闻推送助手。

粗看起来它们的形式和使用工具(插件)各有不同,但是其核心都是工作流——在指定的步骤中完成复杂度不高的任务。

这里的复杂度不高的原因来自三个方面:

  1. 数据有固定的格式
  2. AI处理问题的逻辑是一条直线,没有“岔路”
  3. 处理过程中没有外部的干扰

那缺点就是,只能完成特定场景下的特定问题,AI 含量不高!可能有小伙伴会问了,AI 含量高有什么好处呢?

哈哈,这也是我之前朦胧的地方。

所以,今天就通过一个AI 面试官的应用,探讨在复杂度高一些的场景下,AI 多轮对话应用的实现。

案例:AI 面试官

它的复杂度高在哪里?

  1. 对话内容没有标准格式,并且面试评判标准也不统一。

  2. 面试天然就是一个多轮对话的过程,随着对话深入,对话主题会改变。例如:面试官提问过程就是在从不同维度对候选人综合能力评分的过程。

  3. 真实面试中 可以划分成前后两个阶段。

    前期:面试JD 输入–> 候选人匹配 –> 候选人初筛 –> 约定面试时间。

    面试环节: 候选人介绍自己 –> 面试官根据岗位和候选人背景提出相关面试问题 –> 候选人回答 –> 面试官继续从不同维度对候选人提问 –> 最终形成候选人综合评分(包括:岗位硬技能、软技能、项目经验、发展和稳定度等)

这里,跳过前期沟通阶段,直接进入面试环节开始后的对话和流程。

梳理后,发现完整的一次面试问答至少需要 4轮对话——

如何实现?

参考梳理出的完整对话过程 。如何在一个工作流(workflow)中逐步识别会话重心发生了改变就成为了关键点。

相比于Agent 的方式,workflow中的识别不是依赖LLM 进行语义分析和逻辑判断,而是依赖工作流中多个变量实现——如问题变量、用户回答变量状态的变化。

这里,直接上结论——

对话 需要判断的节点 具体实现
第一轮 对话内容是否和面试相关 对话内容和面试相关:就继续后续步骤。 如果内容和面试不相关就直接答复用户“内容和面试无关,无法答复。”
第二轮 候选人信息和面试岗位信息是否充分 信息充分:就产生面试问题清单。如果信息不充分:就不生成这个清单。
第三轮 面试过程中,候选人 可能只答复其中一条问题,也可能跳着回答 用户回答所有问题:就产生用户回答清单。如果用户没有回答全部问题:就引导用户回答,并不生成回答清单。
第四轮 后续对话,是否基于问答内容生成新的面试问题。 问答内容会放入“记忆”中。当用户回答完所有问题后,会重新进入问题生成环节,该环节会参考之前的记忆。
干扰测试 如果我在 面试过程中 胡言乱语 会发生什么? 明显的胡言乱语,会被第一轮识别过滤掉。如果是在面试过程中文不对题,则会在第三轮用户回答过程中识别。

效果图

  • 完整的工作流

  • 对话日志

    用户针对提出的问题,乱序完成回答后,面试助手会基于问题和用户回答 打分并提出改进建议——

    新一轮对话提出的问题,就参考了之前用户回答中提到项目内容——

    例如:这里就提问了在“AI 知识库”这个项目中,如何平衡数据质量和数据覆盖率的问题。

动手实现

这里,会着重展示如何使用工作流中的多个变量控制会话进入到什么阶段

第一轮 对话

需要筛选与面试相关的内容。如果不相关就回答:“与面试话题无相关。抱歉,我不能答复。”

实现比较简单,直接使用dify的“问题分类器”——

1
2
3
4
if (用户问题 与面试话题有关):
	就进入下一轮话题——生成符合应聘者背景和岗位需求的面试问题
else :
	与面试话题无关的内容,直接回答“不能答复”

第二轮 对话

需要引导用户输入面试所需的人员信息和岗位信息。如果用户一次说不完 就提醒用户补充这些背景信息。

这里的判断逻辑为——

1
2
3
4
5
6
7
8
9
if (全局变量"question" 为空):
	if (模拟面试中所需的面试背景信息充足):
		生成面试问题清单
		存储至变量 question中
		同时,答复用户面试问题
	else:
		引导用户补充更多的面试背景信息
else:
	已经生成面试问题了,可以进入后续步骤——面试问题回答环节	

第三轮 对话

需要检查用户输入的回答对应哪个问题,然后提示用户继续回答全部问题。

提示词——

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 角色
你是一位专业且高效的用户回答与问题匹配智能助手,负责将用户的回答精准匹配到之前给定的问题,并以指定数组形式输出。同时,能从专业HR的视角引导用户完成所有问题回答。

## 背景
当前对话历史:{{ chat_history }}
用户最新答复:{{#sys.query#}}
问题清单:{{#context#}}

## 技能
### 技能 1: 匹配并输出用户回答
1. 仔细查看提供的问题数组,明确问题数量与顺序。
2. 认真阅读用户给出的回答,准确判断该回答对应的问题。
3. 依据用户回答和对话历史,创建一个新数组。在新数组中,按照用户当前及历史回答填充对应问题位置的回答内容,未回答问题的位置用null填充。
4. 最后,将这个新数组以{"userans": [新数组]}的格式输出。

### 技能 2: 检查并提示用户回答所有问题
1. 根据技能1整理好的用户回答数组 “userans”,判断用户未回答的问题。
2. 以专业HR的口吻,礼貌且清晰地提醒用户遗漏的问题,并引导用户回答问题清单上的所有问题。

## 输出示例
在用户初次使用或不明确任务时,按以下示例格式展示匹配输出:
- 示例 1:若问题数组是:["问题 1", "问题 2", "问题 3"],用户的回答是:"这是问题 2 的回答",那么输出应该是:{"userans": ["null","这是问题 2 的回答","null"]}, "more_userans": “请继续回答 问题1 和 问题3”。
- 示例 2:若问题数组是:["问题 A", "问题 B", "问题 C", "问题 D"],用户的回答是:"这是问题 C 的回答",那么输出应该是:{"userans": ["null", "null", "这是问题 C 的回答", "null"]}, "more_userans": “请继续回答 问题A、 问题B 和 问题D”。
- 示例 3:若问题数组是:["问题 1", "问题 2"],用户的回答是:"这是问题 1 的回答。这是问题 2 的回答",那么输出应该是:{"userans": ["这是问题 1 的回答", "这是问题 2 的回答"]}, "more_userans": ""。

## 限制:
- 所有输入和输出均不包含 XML 标签。
- 生成用户回答数组时,内容必须源自用户输入信息,不得编造用户未提及的内容。
- 输出内容必须在structured_output对象中以json格式呈现,严格遵循此框架要求。

## 通用要求:
- 确保用户回答和提问清单准确源自对话过程。
- 语气积极、专业,展现出洞察力。 

当用户回答完所有提问问题后,会存入全局变量“userans” 数组中。

这样就完成了一问一答两个数组(变量)的映射

1
2
3
question = [ "问题1", "问题2", "问题3" ]

userans = [ "这是问题 1 的回答", "这是问题 2 的回答", "这是问题 3 的回答" ]

有了两个数组(变量),后续就可以在循环中,提取每个问题和回答。

简化下判断逻辑,大概长这样——

1
2
3
4
5
6
for id, ques in enumerate(question):
	stand_ans = search "ques" in "HR 问答知识库"
	user_ans = userans[id]
	user_score = LLM (judge user_ans reference stadn_ans)
	Answer.append(user_score)
	

所以,可以直接使用 question 数组中的问题作为控制循环次数和查询知识库的关键字。

第四轮 对话

这里有个小问题,如何判断用户是在继续回答问题,还是在发起新一轮问题?

首先,在第三轮对一问一答两个数组(变量)处理过程中,处理一个问题, question 中就减少一个。全部处理完,question 数组就为空。换个说法就是:如果question 为空,就表示当前是问题产生,而不是问题回答阶段 。

其次,这里用户每次回答的内容userans并没有被清空,而是随着对话变多 而逐步丰富。

所以,只要在第二轮的大模型应用LLM2 中,要求生成问题时,同时参考用户当前输入信息(sys.query)和历史回答信息(userans)就可以产出一轮更深入的问答。

干扰 对话模拟

  • 对话过程中,突然暴躁——说出一句“滚”。

问题分类器会做出回应,答复用户 “与面试话题不相关。抱歉, 不能答复。”

  • 用户回答 开始答非所问。

    LLM4——就是第三轮中的问题识别与关联助手,会识别并关联 用户回答。以及还有哪个问题是没答复的?

下面稍微展开下 使用“RAG” 技术实现面试问答知识库的话题:

为什么需要知识库

使用知识库的原因很简单:LLM 模型在泛化 通用能力上已经可以很好的处理分析问题。例如,写个年终总结报告、生成一段贪吃蛇代码。

但是,如果具体到垂直领域的问题,表现就比较差了。因为它没有这部分领域的准确数据!没有怎么办?大模型就容易出现 “一本正经胡说八道”的情况。此时,问题不在模型不理解、判断不了,而是由于该领域数据不足,无法准确回答。

“知识库”实现了内部私有数据的检索和展示。包含了,数据入库、数据检索和增强LLM上下文 三部分。

先看数据入库阶段——

然后是检索和返回LLM处理过程——

检索模式不止这一种。更多的方式,可以参考:

Advanced RAG Techniques: an Illustrated Overview | by IVAN ILIN | Towards AI

在 dify 中实现面试知识库

大致分为 数据清洗 –> 数据切片 –> 数据向量化&入库 –> 召回验证几个阶段。

1. 拿出准备好的数据集

​ <面试100 问>

2. 数据集 内容预处理(清洗)

经过测试发现,dify 对pdf 文档的处理时无法识别分段标识符(我这里用的 “BBBBBB” 作为分段标识符)。

所以需要先把pdf 转为 word 。然后,使用正则添加分隔符——

同样可以在关键位置添加换行符——

3. 上传文档。数据分段,并使用embedding 工具完成向量化

这里100个问题被划分成了106个数据块(因为有两个问答对中 重复使用了1. 2. 3. 数字开头,被分段标识符 “BBBBBB” 多划分出了几个段落)

4. 指定 索引和混合检索方式

5. 召回验证

​ AI 面试官案例中,主要是希望让大模型参考专业HR的标准回答思路,而不是具体内容。

​ 所以 重点参考回答思路中内容。

最后,RAG在哪些场景中效果不好

RAG 技术中很关键的步骤就是 根据输入的内容 快速检索出 与之接近的(向量)数据块。

为了更好的检索效果,引入了混合检索和重排技术。在检索时,同时根据问题向量(vector index)和语义(summary index)召回数据片段。这里权重比为 7:3。

例如使用之前的面试问题提问,查看召回内容:

但是,如果有以下三种情况还是容易出现数据检索不出来的情况:

  1. 相同的关键字 散落在文档里很多地方。

    例如,小说中的 主人公“许仙”。提问:“许仙和白素贞在断桥有几次相遇?” RAG即使检索出来,也无法保证召回的片段覆盖了所有相遇的剧情。

  2. 检索语义不是通用的,而是带有垂直领域的内容。

    例如,”那个在<还珠格格>中饰演尔康的演员还演过其他的什么影视剧?”

    这种知识就很难检索出来——“饰演尔康的演员”本身又是另一个检索的结果。知识库得先检索出这个演员的真实名字 ,还要存有每部电视剧的 演员-角色 关系表,然后才可以通过演员信息 检索出所有出演的影视剧。

  3. 还有一种场景,也比较常见:知识库本身质量不高。

    例如:文档格式不统一,文档覆盖面不足,导致检索的时候 得到的内容离问题相关度很远。

    此时,属于RAG 技术也帮不了的情况。

前两种是 RAG 机制的导致的,无法避免。最后,这个属于领域知识数据 没达到可用的程度。

除了RAG 还经常听说模型微调?

下面是它们两者间的相同和不同的地方——

相同点 不同点
补充模型在某个领域特定知识,解决幻觉问题 实现路径不一样
RAG 类比:开卷考试——考试时现查 根据问题去知识库检索相似内容或案例
微调 类比:考前复习——把之前考试真题全部做一遍 通过多轮训练,强化模型在该领域的能力(不仅仅是数据上的,还有推理和逻辑上的针对性强化)

彩蛋

  • 同样的案例,后续会使用n8n 的Agent模式再实现一遍。

    到时会豁然开朗:在多轮对话场景中,workflow 和 Agent 有什么差异;以及深入到RAG 调用环节的细节。

  • 从产品角度看,AI 应用效果的量化很重要。即 AI 应用内部每个节点状态的可观测性是非常必要的。

    以下是通过可观测工具 Langfuse 快速筛选出 “过去三天中 所有执行失败”的那些对话——

    是不是非常方便?有了这些对话,为下一步AI 产品迭代升级 提供了基础的数据支撑。

    举个例子:用户反应某次对话执行时报错了。那到底是提示词问题,还是大模型响应超时,还是发生了预期外的问题,是不是得有个统计数据在这里呢?

Licensed under CC BY-NC-SA 4.0