京东 算法工程师-C++ 一面

1. 先说一下你对 LLM 的基本理解,预训练、指令微调和对齐分别在做什么

LLM 本质上是一个大规模参数化语言模型,训练目标通常是根据上下文预测下一个 token。预训练阶段主要靠海量通用语料学习语言规律、知识分布和上下文建模能力,这一步决定模型的基础能力。指令微调是在预训练模型之上,用更贴近问答、助手式交互的数据继续训练,让模型学会“按要求回答”。对齐通常是进一步让模型输出更符合人类偏好和安全要求,比如减少胡说、提升可控性、避免明显有害输出。如果从工程角度看,预训练决定上限,微调决定风格和任务适配,对齐决定可用性。

2. RAG 的核心流程是什么,为什么它能缓解大模型幻觉

RAG 一般分成文档切分、向量化、索引构建、召回、重排和生成几个阶段。用户提问后,系统先把问题编码成向量,在知识库里召回相关片段,再把这些片段连同问题一起喂给大模型生成答案。它能缓解幻觉的核心原因是把“只靠参数记忆回答”变成了“基于外部证据回答”。不过 RAG 并不是万能的,如果切块不合理、召回不准、上下文污染严重,模型照样会答偏。所以真正难的地方通常不是“接个向量库”,而是检索质量、上下文组织和答案约束。

3. 如果让你优化一个 RAG 系统的效果,你会优先看哪些环节

我会先判断问题出在召回、排序还是生成。如果相关文档根本没被召回来,那重点要看 chunk 大小、切分策略、embedding 模型、召回 topk 和索引质量。如果召回了但最终答案还是不准,可能要看重排模型是否把真正相关内容排到了前面,以及上下文拼接是否把噪声带进去了。如果模型明明拿到了正确证据却仍然回答错误,那就要看 prompt 约束、引用格式、答案生成模板,必要时做基于证据的输出限制。RAG 调优通常是个链路问题,单看大模型本身往往定位不到根因。

4. 进程、线程、协程分别是什么,它们的本质区别在哪里

进程是资源分配的基本单位,拥有独立地址空间和系统资源,隔离性最强。线程是 CPU 调度的基本单位,同一进程内的线程共享地址空间和大部分资源,通信方便,但同步复杂。协程通常是用户态的轻量级执行单元,不由内核直接调度,而是由运行时或程序自己切换。进程切换代价通常最大,线程次之,协程最轻。如果是高并发 I/O 场景,协程能明显降低阻塞等待带来的线程成本;如果强调多核并行执行,线程仍然是更直接的手段。

5. 线程同步一般怎么做,互斥、条件同步和原子操作分别适合什么场景

线程同步常见方式有互斥锁、读写锁、条件变量、信号量和原子操作。互斥锁适合保护临界区,保证同一时刻只有一个线程修改共享数据。条件变量适合线程之间的等待和通知,比如生产者消费者模型。信号量更适合控制资源数量,比如连接池、任务槽位。原子操作适合非常轻量的共享状态更新,比如计数器、标志位,但原子并不意味着可以替代所有锁,因为复杂复合逻辑往往仍需要互斥保护。

代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
using namespace std;

mutex mtx;
condition_variable cv;
queue<int> q;
bool done = false;

void producer() {
    for (int i = 1; i <= 5; ++i) {
        {
            lock_guard<mutex> lock(mtx);
            q.push(i);
        }
        cv.notify_one();
    }
    {
        lock_guard<mutex> lock(mtx);
        done = true;
    }
    cv.notify_all();
}

void consumer() {
    while (true) {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [] { return !q.empty() || done; });
        while (!q.empty()) {
            cout << q.front() << endl;
            q.pop();
        }
        if (done) break;
    }
}

6. C++ 里常见的锁有哪些,它们底层大致是怎么实现的

常见的有 std::mutexstd::recursive_mutexstd::shared_mutexstd::timed_mutex,以及基于原子操作实现的自旋锁。mutex 最常用,适合普通互斥场景;recursive_mutex 允许同一线程重复加锁,但通常不建议滥用;shared_mutex 适合读多写少;定时锁可以做超时控制。从底层看,用户态通常先尝试通过原子 CAS 抢锁,如果竞争不激烈,可能直接在用户态完成;竞争激烈时会进入内核阻塞和唤醒,这也是为什么锁竞争会带来上下文切换开销。自旋锁适合锁持有时间很短的场景,因为它不睡眠而是原地忙等,但如果临界区长,自旋会浪费 CPU。所以锁的选择不只是 API 问题,本质上是等待成本和临界区长度的权衡。

7. malloc 和 new 有什么区别,free 和 delete 为什么不能混用

malloc 是 C 的内存分配函数,只负责按字节申请原始内存,不会调用构造函数;new 是 C++ 运算符,除了分配内存,还会调用对象构造函数。对应地,free 只释放内存,不会调用析构函数;delete 会先调用析构,再释放内存。两者底层都可能最终走到分配器,但语义层完全不同,所以不能混用。比如 malloc 得到的对象没有被构造,直接 delete 是错误的;new 创建的对象如果用 free 释放,则析构不会执行,也会破坏运行时管理逻辑。

代码:


 

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务