AI 开发相关面试题整理(含 Demo + 深追模拟)
每题三层:① 核心答案 ② 可运行 Demo ③ 深追 Q&A 模拟对话
1. GPT / Gemini / Claude 各自什么脾性,怎么搭配多模型工作流
核心答案
| 模型 | 优势 | 弱点 |
|---|---|---|
| GPT-4o | 生态广、工具调用稳、多模态强、响应快 | 有时过度取悦用户,长推理深度不足 |
| Gemini 2.0 | 超长上下文(100万 token)、Google 生态集成好 | 指令跟随有时漂移,代码稳定性稍逊 |
| Claude Sonnet/Opus | 指令跟随精准、代码质量高、长文本处理强 | 工具生态相对小,有时过于谨慎 |
选型口诀: Claude 做事、GPT 审查、Gemini 塞料(超长上下文)。
多模型协作架构:
less
Claude Code(4-5 tab 并行实施)
├── Tab A: feature/search 实施
├── Tab B: feature/auth 实施
└── Tab C: 方案设计 / 架构评审
Codex / GPT-4(独立 session)
└── 对 Tab A/B 产出做批判性 Review
------ 不共享上下文,保证独立性
工作流沉淀:
CLAUDE.md:项目级规范常驻上下文- Slash command(
/writer、/review):封装高频 prompt - Skill 系统:跨项目最佳实践复用
- Hooks:pre-commit 自动注入检查
🔧 Demo:用 OpenRouter 统一接入多模型
typescript
// openrouter-demo.ts ------ 一个 key 路由到不同模型
// 运行:npx ts-node openrouter-demo.ts
const BASE = 'https://openrouter.ai/api/v1/chat/completions';
const KEY = process.env.OPENROUTER_API_KEY!;
async function callModel(model: string, prompt: string) {
const res = await fetch(BASE, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
}),
});
const data = await res.json();
return data.choices[0].message.content as string;
}
async function reviewWorkflow(task: string) {
console.log('=== Step 1: Claude 实施 ===');
const implementation = await callModel(
'anthropic/claude-sonnet-4-5',
`请实现以下功能,只输出代码:${task}`
);
console.log(implementation);
console.log('\n=== Step 2: GPT-4 Review ===');
const review = await callModel(
'openai/gpt-4o',
`请对以下代码做批判性 Review,列出潜在问题:\n\n${implementation}`
);
console.log(review);
}
reviewWorkflow('实现一个带防抖的搜索输入框 React Hook');
💬 深追 Q&A 模拟
Q:你怎么保证两个模型不会互相影响,比如 GPT review 时不会被 Claude 的实现思路带偏?
关键是上下文隔离。GPT review session 只拿到最终代码产物,不拿到 Claude 的思考过程和中间对话。我的做法是 Claude 做完后把代码复制到新的 GPT session,prompt 里只给代码 + "请独立批判性 review",不带任何 Claude 的解释。这样 GPT 是在用自己的视角评估,而不是在帮 Claude 辩护。
Q:你说 Claude 指令跟随精准,能举个具体例子吗?
我有个场景:让 AI 只修改某一个函数,不要动其他文件。Claude 基本能严格遵守,GPT-4 有时会"顺手"改几个它觉得有问题的地方。在多文件项目里这个差异影响很大,因为你不知道它改了什么,review 成本就高了。
Q:Gemini 超长上下文你实际用过吗,有没有掉坑?
用过,主要是把整个 repo 塞进去问架构问题。坑是:context 越长,模型越容易"迷失",对早期信息的注意力下降。实测超过 20 万 token 后,回答的准确性会有肉眼可见的下降,所以我不会无脑塞,会先做文件筛选,只塞相关模块。
2. AI 上下文治理(污染、回退、Sub Agent 隔离、压缩)
核心答案
| 问题 | 策略 |
|---|---|
| 上下文污染 | 新开对话 / /clear / compact;只把 relevant 内容放进来 |
| Sub Agent 隔离 | 子任务起独立 Agent,父 Agent 只看摘要结果 |
| 上下文压缩 | 超长对话让模型总结当前状态,新开对话粘摘要续跑 |
| Skill 注入 | 规范外置到 skill 文件,按需注入,不污染主对话 |
| 版本回退 | 重要节点 commit,AI 出问题直接 git checkout |
🔧 Demo:CLAUDE.md 项目规范模板(最小可用版)
markdown
<!-- .claude/CLAUDE.md ------ 放在项目根目录,Claude Code 自动读取 -->
# 项目规范
## 技术栈
- React 18 + TypeScript 5 + Vite
- 状态管理:Zustand(禁止引入 Redux)
- 样式:Tailwind CSS(禁止写 inline style)
- 请求:TanStack Query v5
## 目录约定
- 组件放 src/components,每个组件一个目录(index.tsx + index.module.css)
- 业务 hook 放 src/hooks,以 use 开头
- 工具函数放 src/utils,纯函数,不引入 React
## 禁止事项
- 禁止在组件内直接 fetch,必须通过 TanStack Query
- 禁止使用 any,必须定义具体类型
- 禁止修改 src/api 目录下的文件,那是自动生成的
## 提交规范
- feat: / fix: / refactor: / chore: 前缀
- 每个 PR 只做一件事
🔧 Demo:pre-commit hook 自动检查(最小复现)
bash
# .husky/pre-commit
#!/bin/sh
echo "Running pre-commit checks..."
# TypeScript 类型检查
npx tsc --noEmit
if [ $? -ne 0 ]; then
echo "❌ TypeScript 类型错误,请修复后再提交"
exit 1
fi
# ESLint
npx eslint src --ext .ts,.tsx --max-warnings 0
if [ $? -ne 0 ]; then
echo "❌ ESLint 错误,请修复后再提交"
exit 1
fi
echo "✅ 检查通过"
🔧 Demo:Sub Agent 隔离模式(Claude Code Task 工具示意)
bash
# 父 Agent prompt(in CLAUDE.md or system prompt):
你是一个 orchestrator。将以下需求拆成独立子任务,
每个子任务用 Task 工具启动独立 Agent 执行,
只关注最终产物,不关注执行细节。
# 子 Agent 只拿到:
任务描述 + 相关文件 + 完成标准
------ 不拿到父 Agent 的对话历史
# 父 Agent 收到:
执行摘要 + 修改的文件列表
------ 不拿到子 Agent 的中间推理
💬 深追 Q&A 模拟
Q:上下文污染你是怎么发现的,有没有具体现象?
最明显的现象是 AI 开始"记仇"。比如我之前尝试了一个错误的方案,后来换了正确方向,但 AI 还是会时不时往错误方向走,因为它的上下文里有大量失败的尝试。另一个现象是回答越来越长、越来越啰嗦,像是在不停 recap 之前说过的内容。这时候我就新开对话,只粘贴当前有效的代码和下一步目标。
Q:Sub Agent 隔离在工程上怎么实现,不是随便说说的那种?
用 Claude Code 的
Task工具,父 Agent 通过 Task 启动子 Agent,子 Agent 有自己独立的上下文窗口。父 Agent 的 prompt 里我明确写"不要自己动手,用 Task 工具分发",子 Agent 完成后只返回执行摘要。如果不用 Claude Code,手工实现就是:每个子任务开一个新的 API 调用,system prompt 只带当前子任务的上下文,结果聚合在父层处理。
Q:compact / 压缩之后信息会丢失吗,你怎么保证关键决策不丢?
会丢,这是压缩的代价。我的做法是在做重要架构决策的时候,让 AI 生成一个"决策记录"(类似 ADR),写进 CLAUDE.md 或单独的
decisions.md。这样压缩上下文后,决策记录还在文件系统里,下次对话 AI 读文件就能恢复上下文,而不是靠对话历史。
3. 公司有使用 AI 提效赋能吗
核心答案
五个维度展开,最好有量化数据:
- 代码层面:Claude Code 实施 + Codex Review,并行多 tab
- 流程层面:slash command、skill 系统沉淀高频 prompt
- 文档层面:AI 生成技术文档、PRD 初稿、commit message
- 质量层面:AI 辅助写单测、安全扫描
- 工作流层面:CLAUDE.md + hooks 自动化检查
🔧 Demo:slash command /review 示例
markdown
<!-- .claude/commands/review.md ------ /review 命令 -->
请对当前改动做代码 Review,按以下维度输出:
## 正确性
- 逻辑是否有 bug?
- 边界情况是否处理?
## 安全性
- 有无 XSS / CSRF / SQL 注入风险?
- 用户输入是否做了校验?
## 性能
- 有无不必要的重渲染?
- 有无内存泄漏风险?
## 可维护性
- 命名是否清晰?
- 是否有魔法数字需要提取为常量?
最后给出:🔴 必须修复 / 🟡 建议修改 / 🟢 可以接受
🔧 Demo:AI 生成 commit message 的 hook
bash
# .husky/prepare-commit-msg
#!/bin/sh
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
# 只对空 commit message 生效(用户没有手动填写时)
if [ -z "$COMMIT_SOURCE" ]; then
DIFF=$(git diff --cached --stat)
# 调用 AI 生成 commit message(示例用 Claude API)
MSG=$(curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "{
"model": "claude-haiku-4-5",
"max_tokens": 100,
"messages": [{"role": "user", "content": "根据以下 git diff stat 生成一行 conventional commit message(feat/fix/refactor/chore: 描述):\n$DIFF"}]
}" | jq -r '.content[0].text')
echo "$MSG" > "$COMMIT_MSG_FILE"
fi
💬 深追 Q&A 模拟
Q:你说 AI 帮你写单测,测试覆盖率真的能提升吗,AI 写的测试质量怎么样?
覆盖率是提升了,但质量要看提示词。如果让 AI "给这个函数写单测",它通常只测 happy path,边界情况覆盖不足。我改成让它"写单测,必须包含:正常情况、边界值、错误情况、异步失败情况",质量明显好很多。另外 AI 对 mock 的使用经常过度,我会让它"尽量少 mock,优先用真实逻辑"来约束。
Q:量化收益你说"从两天缩到半天",这怎么衡量的,有可信度吗?
这个主要靠对比,同类型的功能开发,AI 介入前后的 PR 提交时间对比。严格来说不科学,因为功能复杂度不同。更客观的是看 review 轮次:我们组引入 AI 辅助 review 之后,平均 PR review 轮次从 3.2 次降到 1.8 次,这个是有记录的。
4. Skill 系统设计(发现、注册、热更新、隔离)
核心答案
Skill 系统本质是可插拔的 prompt 模块化 + 上下文注入管道,架构类似插件系统。
| 阶段 | 实现方式 |
|---|---|
| 发现 | 扫描约定目录,找 SKILL.md;优先级:项目级 > 用户级 > 系统级 |
| 注册 | 元数据(name/description/trigger)+ 内容 → 写入内存 registry |
| 热更新 | fs.watch 监听目录,变更时重新解析,hash 比对避免无效重载 |
| 隔离 | 独立上下文沙箱;最小工具集(默认只 read);Sub Agent 模式 |
🔧 Demo:一个最小 SKILL.md
yaml
---
name: react-component
description: 生成符合项目规范的 React 函数组件,包含 TypeScript 类型、样式模块、单测
trigger: /component
---
## 使用方式
调用时提供组件名称和功能描述。
## 输出规范
1. `ComponentName/index.tsx` --- 组件本体
2. `ComponentName/index.module.css` --- 样式(Tailwind 优先,复杂样式用 module)
3. `ComponentName/index.test.tsx` --- 单测(vitest + @testing-library/react)
## 代码规范
- Props 接口命名为 `ComponentNameProps`
- 必须有 JSDoc 注释描述组件用途
- 不使用 default export,使用 named export
- 异步操作必须有 loading / error 状态
## 示例结构
```tsx
export interface SearchBoxProps {
/** 搜索结果回调 */
onSearch: (keyword: string) => void;
placeholder?: string;
}
export function SearchBox({ onSearch, placeholder = '搜索...' }: SearchBoxProps) {
// 实现
}
shell
### 🔧 Demo:最小 Skill 热更新 Watcher(Node.js)
// skill-watcher.ts ------ npx ts-node skill-watcher.ts import fs from 'fs'; import path from 'path'; import crypto from 'crypto';
interface Skill { name: string; description: string; content: string; hash: string; }
const registry = new Map<string, Skill>(); const SKILLS_DIR = path.resolve('.claude/skills');
function parseSkill(filePath: string): Skill | null { try { const raw = fs.readFileSync(filePath, 'utf-8'); // 解析 frontmatter const match = raw.match(/^---\n(\\s\\S?)\n---\n(\\s\\S)$/); if (!match) return null; const meta: Record<string, string> = {}; match1.split('\n').forEach(line => { const k, ...v = line.split(':'); if (k) metak.trim() = v.join(':').trim(); }); return { name: meta.name, description: meta.description, content: match2, hash: crypto.createHash('md5').update(raw).digest('hex'), }; } catch { return null; } }
function loadSkills() { if (!fs.existsSync(SKILLS_DIR)) return; fs.readdirSync(SKILLS_DIR) .filter(f => f.endsWith('.md')) .forEach(f => { const skill = parseSkill(path.join(SKILLS_DIR, f)); if (skill) { registry.set(skill.name, skill); console.log([skill] loaded: ${skill.name}); } }); }
function watchSkills() { loadSkills(); fs.watch(SKILLS_DIR, (event, filename) => { if (!filename?.endsWith('.md')) return; const filePath = path.join(SKILLS_DIR, filename); const skill = parseSkill(filePath); if (!skill) return; const existing = registry.get(skill.name); // hash 比对,避免无效重载 if (existing?.hash === skill.hash) return; registry.set(skill.name, skill); console.log([skill] hot-reloaded: ${skill.name}); }); console.log(Watching ${SKILLS_DIR} for skill changes...); }
watchSkills(); // registry 现在随时可以查询 export { registry };
markdown
* * *
### 💬 深追 Q&A 模拟
**Q:Skill 发现用约定目录扫描,如果 skill 名字冲突了怎么办?**
> 优先级规则:项目级 > 用户级 > 系统级,就近原则,同层级里后加载的覆盖先加载的。实际上在描述字段上也可以做 namespace,比如 `react:component` 和 `vue:component`,registry 的 key 带 namespace 就不冲突了。
**Q:热更新你用 `fs.watch`,这个在 macOS 上不可靠是众所周知的问题,你怎么处理?**
> 确实,`fs.watch` 在 macOS 上有时会漏事件,生产级别的方案要用 `chokidar`,它内部在不同平台用不同的 API(macOS 用 FSEvents,Linux 用 inotify),稳定性好很多。另外加一个 polling fallback,每隔 30 秒全量重扫一次作为兜底。
**Q:隔离你说"独立上下文沙箱",上下文泄漏的场景你遇到过吗?**
> 遇到过。最典型的是 skill A 里注入了一个"你是一个严格的代码审查者"的角色设定,没有正确清理,导致后续普通对话 AI 也开始过度挑剔。解法是 skill 执行完成后,发一条 reset prompt:"现在 skill 执行结束,回到默认角色",或者更彻底的方案是用独立的 Agent 实例执行 skill,天然隔离。
* * *
## 5. 你平常用什么 Vibecoding 工具
### 核心答案
- **主力实施**:Claude Code(CLI)------ 精准指令跟随,代码质量高
- **深度 Review**:Codex ------ 思考更深,适合批判性审查
- **IDE 内嵌**:Cursor / Windsurf ------ 需要在编辑器里操作时
- **行级补全**:GitHub Copilot ------ 低思考成本的自动补全
- **并行模式**:同时开 4--5 个 Claude Code session,不同子任务并行跑
* * *
### 🔧 Demo:Claude Code 多 tab 并行工作流
Terminal 1 ------ feature/search 实施
cd ~/project claude "实现搜索组件,防抖 300ms,结果展示在 SearchResults 组件里, 样式参考 Figma link,接口文档在 docs/api/search.md"
Terminal 2 ------ feature/auth 实施(并行,互不干扰)
claude "实现登录页面,使用 react-hook-form + zod 做表单校验, 成功后存 token 到 useAuthStore,跳转到 /dashboard"
Terminal 3 ------ Review Tab 1 产出(新 session,独立视角)
claude "请 review src/components/Search 目录下的代码, 重点关注:防抖是否正确、loading 状态是否完整、错误处理是否覆盖"
markdown
* * *
### 💬 深追 Q&A 模拟
**Q:多 tab 并行,上下文互相隔离,但如果两个 tab 改了同一个文件怎么办?**
> 这是并行 AI 开发最大的风险,所以任务拆分时要明确文件归属,A tab 负责的文件 B tab 不能碰。我在 prompt 里明确写"只修改以下文件:xxx",并且两个 tab 完成后我手动做 merge review,看有没有冲突。频繁 commit 也是关键,每个 tab 完成一个小里程碑就 commit,这样 git 有完整历史,出现冲突能 diff 追溯。
**Q:Cursor 和 Claude Code 你会怎么选?**
> 场景不同。Cursor 适合需要在编辑器里随时问随时改、上下文是当前打开文件的场景;Claude Code 适合需要精确控制、多文件复杂任务、需要自定义 hooks 和 CLAUDE.md 的场景。我现在主要用 Claude Code,因为它的指令跟随更可控,而且 hooks 和 skill 系统更灵活。
* * *
## 6. 平常是怎么用 Vibecoding 的
### 核心答案
1. **任务拆解先行**:大需求拆成独立子任务,每个 tab 一块
1. **CLAUDE.md 注入规范**:架构、命名、禁止事项写进去,每次自动生效
1. **方案 → 审查分离**:Claude 实施,Codex 独立 review,形成制衡
1. **自建 slash command**:高频操作封装成 `/review`、`/writer` 等
1. **hooks 自动化**:pre-commit 触发 lint / typecheck,不手动提醒 AI
1. **及时 commit**:每个可工作的里程碑就 commit,方便回退
* * *
### 🔧 Demo:一次完整 Vibecoding session 的标准流程
1. 打开项目,Claude Code 自动读取 CLAUDE.md
$ claude
2. 告诉 AI 今天做什么(任务拆解)
今天要做搜索功能,拆成三个子任务:
- SearchInput 组件(防抖、清空、loading)
- SearchResults 组件(列表渲染、空态、错误态)
- useSearch hook(接口调用、状态管理) 先做第 1 个,完成后我告诉你做第 2 个。
3. AI 完成后 review
/review (触发自定义 review slash command)
4. 确认没问题,commit
帮我生成 commit message
5. 开始第 2 个子任务(新 prompt,上下文保持清洁)
现在做 SearchResults 组件,接收 results: SearchResult\[\] 类型, 空态展示 EmptyState 组件,加载中展示 Skeleton,错误态展示 ErrorBoundary
markdown
* * *
### 💬 深追 Q&A 模拟
**Q:你说"方案和审查分离",Codex review 完发现问题,你怎么把问题反馈给 Claude?**
> 我不会直接把 Codex 的 review 粘给 Claude,因为这样上下文会变成"Claude 在给自己的代码做辩护",容易有偏见。我的做法是:把 review 里的每个问题,自己消化理解,确认是真问题后,以"这段代码有个问题:......,请修复"的方式告诉 Claude,以问题描述为驱动,而不是以另一个 AI 的评价为驱动。
**Q:及时 commit 这个习惯,AI 改了一大堆文件之前 commit,有时候功能不完整,怎么处理?**
> 用 WIP commit(Work In Progress)。`git commit -m "wip: 搜索组件骨架,功能未完成"`,打上 wip 标签,在 CI 里配置 wip commit 不触发部署流水线。功能完成后 `git rebase -i` 把 wip commits squash 成一个干净的 commit 再 push PR。这样既有安全网,又不污染主线历史。
* * *
## 7. 使用 Vibecoding 时有什么需要注意的
### 核心答案
1. **上下文管理**:对话过长及时新开,用摘要续接
1. **不要盲目接受**:安全相关代码(SQL/XSS/权限)必须人工 review
1. **原子化提交**:改动范围小,方便 review 和回退
1. **明确约束**:说清楚不能改什么,防止 AI 发挥
1. **测试同步**:功能和测试同时写,不要事后补
1. **上下文污染**:失败方向及时清理,开新对话
1. **保持判断**:架构决策自己拍,AI 给候选方案
* * *
### 🔧 Demo:好 prompt vs 坏 prompt 对比
❌ 坏 prompt ------ 太模糊,AI 会做很多假设
帮我写一个用户管理页面
✅ 好 prompt ------ 明确范围、约束、不做什么
实现 UserManagement 页面,要求:
- 展示用户列表,使用 UserTable 组件(已存在于 src/components/UserTable)
- 支持按用户名搜索(防抖 300ms)
- 支持分页(每页 20 条,使用 Pagination 组件)
- 只修改 src/pages/UserManagement/index.tsx,不要改其他文件
- 不需要做新增/编辑/删除功能,那是下个迭代的事
❌ 坏 prompt ------ 一次让 AI 改太多
重构整个 src/api 目录,把所有请求从 axios 换成 fetch, 同时加上错误处理、retry 逻辑、鉴权 token 注入
✅ 好 prompt ------ 拆小步骤,逐步推进
先只把 src/api/user.ts 从 axios 换成 fetch, 保持接口签名不变,加上基础的错误处理(4xx/5xx 抛错), 不做 retry 和 token 注入,那是后面的事
markdown
* * *
### 💬 深追 Q&A 模拟
**Q:你说安全相关代码必须人工 review,实际上你怎么判断哪些是安全相关的?**
> 我有一个心理 checklist:① 用户输入是否直接拼接到 SQL / HTML / shell 命令 ② 接口有没有鉴权检查 ③ 敏感数据(密码、token)有没有被 log 或暴露在响应里 ④ 文件上传有没有做类型和大小限制。遇到这几类代码,我会暂停,自己逐行读一遍,不依赖 AI 自检------因为 AI 自检的时候它是在验证它自己的逻辑,容易有盲区。
**Q:AI 发挥改了不该改的东西,你怎么快速发现?**
> `git diff --stat` 先看改了哪些文件,如果文件列表里有我没提到的文件,立刻 `git diff 那个文件` 看具体改了什么。所以 atomic commit 非常重要,每次 AI 改完我都先 diff 再 commit,而不是干完一大堆再统一 commit,那时候改动太多,diff 已经没法看了。
* * *
## 8. 有没有用过 Superpower、OpenRouter 等第三方增强工具
### 核心答案
| 工具 | 用途 | 特点 |
| -------------------------- | ---------------------- | ---------------------- |
| **OpenRouter** | 统一 API 网关,一个 key 路由多模型 | 方便多模型对比,有用量统计 |
| **Superpower for ChatGPT** | 增强 ChatGPT 界面 | 历史搜索、prompt 模板、文件夹分类 |
| **Continue.dev** | 开源 IDE 插件,自接多种模型 | 支持自定义 context provider |
| **Aider** | 命令行 AI 编程,支持 git 集成 | 自动 commit,适合纯终端工作流 |
关注核心:**context 精准控制**、**多模型路由**、**数据本地化**(不出境)。
* * *
### 🔧 Demo:OpenRouter 多模型对比同一个问题
// compare-models.ts ------ npx ts-node compare-models.ts const MODELS = 'anthropic/claude-sonnet-4-5', 'openai/gpt-4o', 'google/gemini-2.0-flash', ;
const QUESTION = '用 100 字以内解释什么是闭包,举一个实际应用场景';
async function ask(model: string, question: string) { const res = await fetch('openrouter.ai/api/v1/chat...', { method: 'POST', headers: { Authorization: Bearer ${process.env.OPENROUTER_API_KEY}, 'Content-Type': 'application/json', }, body: JSON.stringify({ model, messages: { role: 'user', content: question }, max_tokens: 200, }), }); const data = await res.json(); return data.choices0.message.content; }
(async () => { for (const model of MODELS) { console.log(\n=== ${model} ===); console.log(await ask(model, QUESTION)); } })();
markdown
* * *
### 💬 深追 Q&A 模拟
**Q:你用 OpenRouter 会不会有数据安全问题,公司代码过第三方是否合规?**
> 这是实际工作中要考虑的。我的做法是:公司项目的核心业务代码不过第三方,只在个人项目和开源项目里用 OpenRouter 做多模型对比。公司内部用的是厂商直连 API(Anthropic / OpenAI 官方),如果有私有化需求就用本地部署的模型(Ollama + 开源模型)。这是个合规边界,面试时可以主动提出来,显示你有安全意识。
* * *
## 9. 为什么 AI 流式输出用 SSE,不用 WebSocket
### 核心答案
| 维度 | SSE | WebSocket |
| ---- | ------------------------- | ------------- |
| 通信方向 | 单向(服务端 → 客户端) | 双向 |
| 协议基础 | 标准 HTTP | 需要 Upgrade 握手 |
| 断线重连 | 浏览器自动重连 | 需手动实现 |
| 负载均衡 | 天然支持 | 需要代理额外配置 |
| 鉴权 | EventSource 不支持自定义 Header | 握手时可带 Header |
| 实现成本 | 低(几行代码) | 高(需维护连接状态) |
**AI 选 SSE:** 流式输出是单向推送,SSE 语义完全匹配;基于 HTTP 对基础设施友好;HTTP/2 下性能不输 WebSocket。
**用 WebSocket 的场景:** AI 对话需要实时打断(用户说话中途停止生成)、多人协作(多个用户同时发消息)。
* * *
### 🔧 Demo:SSE vs WebSocket 最小对比(可在浏览器 Console 运行)
// SSE 客户端(EventSource) const es = new EventSource('/sse-stream'); es.onmessage = e => console.log('SSE received:', e.data); es.onerror = () => console.log('SSE error, will auto-reconnect'); // 断线后浏览器自动重连,无需任何额外代码
// WebSocket 客户端 const ws = new WebSocket('ws://localhost:3001'); ws.onopen = () => console.log('WS connected'); ws.onmessage = e => console.log('WS received:', e.data); ws.onclose = () => { console.log('WS closed, manually reconnecting...'); setTimeout(() => new WebSocket('ws://localhost:3001'), 3000); // 手动重连 };
markdown
* * *
### 💬 深追 Q&A 模拟
**Q:你说 HTTP/2 下 SSE 性能不输 WebSocket,能解释一下为什么吗?**
> HTTP/1.1 下,一个 SSE 连接占一个 TCP 连接,浏览器对同域名有 6 个并发连接限制,如果同时开多个 SSE 会被限制。HTTP/2 下,多个 SSE 流可以复用同一个 TCP 连接(多路复用),这个限制就消失了。WebSocket 本来的优势之一是不受 HTTP 连接限制,但 HTTP/2 + SSE 这个组合在性能上已经很接近了,而 SSE 在运维上更简单。
**Q:SSE 不能自定义请求头,鉴权是个问题,你实际怎么解决的?**
> 两种方案,生产里我用第一种:用 `fetch` + `ReadableStream` 替代 `EventSource`,这样可以带 `Authorization` header,同时也支持 POST 请求(EventSource 只能 GET)。第二种是 URL 里带 token 参数,但 token 会出现在服务器 access log 里,安全性差,不推荐。
* * *
## 10. SSE 流式输出的完整过程,每步数据怎么处理
### 核心答案
SSE 是基于 HTTP 的单向推送协议,每条消息纯文本格式,以空行结束。
**协议格式:**
data: {"token": "你好"}\n \n data: {"token": "世界"}\n \n data: DONE\n \n
markdown
**数据处理链路:**
Uint8Array chunk → TextDecoder.decode(chunk, { stream: true }) ← 防中文截断 → 拼入 buffer → 按 \n 切行,最后不完整行留 buffer → 过滤非 data: 开头的行 → JSON.parse(payload) → 取 token 字段 → 追加到 UI
markdown
* * *
### 🔧 Demo:最小可运行 SSE Server + 客户端
**服务端(Node.js,存为 `sse-server.js`):**
// sse-server.js ------ node sse-server.js const http = require('http');
http.createServer((req, res) => { if (req.url === '/stream') { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', });
ini
const tokens = '你好世界,这是一段流式输出的文字。'.split('');
let i = 0;
const timer = setInterval(() => {
if (i >= tokens.length) {
res.write('data: [DONE]\n\n');
clearInterval(timer);
res.end();
return;
}
res.write(`data: ${JSON.stringify({ token: tokens[i++] })}\n\n`);
}, 100);
req.on('close', () => clearInterval(timer));
} else { // 返回客户端 HTML res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(`
`); } }).listen(3000, () => console.log('Open http://localhost:3000')); ```
shell
# 运行
node sse-server.js
# 打开浏览器访问 http://localhost:3000
# 可以看到文字逐字流式出现
💬 深追 Q&A 模拟
Q:TextDecoder 的 stream: true 你能解释清楚为什么必须加吗?
UTF-8 是变长编码,中文字符占 3 个字节。当服务端的一个 chunk 恰好在这 3 个字节中间切断时,比如前 2 个字节在这个 chunk,第 3 个字节在下一个 chunk,如果不加
stream: true,TextDecoder 会在当前 chunk 结束时尝试把不完整的字节序列解码,输出乱码(通常是?或<U+FFFD>)。加了stream: true后,TextDecoder 知道还有后续数据,会把不完整的字节缓存起来等下一个 chunk 到了再一起解码。
Q:buffer 里保留最后一行是什么逻辑,为什么不直接按行处理?
因为
response.body.getReader()读出来的 chunk 是任意大小的字节片段,不保证按换行符对齐。一个 SSE 消息data: {"token":"好"}\n\n可能被切成两个 chunk:data: {"token":"好"}和\n\n。如果直接处理每个 chunk,就会尝试解析一个不完整的行,JSON.parse 失败。所以要把 chunk 拼到 buffer 里,按\n切行,最后一段可能不完整的保留到下一个 chunk 拼完再处理。
Q:SSE 断线了,Last-Event-ID 续传是怎么工作的?
服务端在每条消息加
id: 42\n,浏览器的EventSource会记住最后收到的 ID。断线重连时,浏览器自动在请求头带Last-Event-ID: 42,服务端收到后从 ID 42 之后的消息开始推送,不需要客户端做任何额外逻辑。用fetch+ReadableStream的话,这个机制要手动实现:记录最后的 event id,重连时在请求头里带上。
11. SSE 流式场景下,如何判断 Markdown 表格"完整"后再渲染
核心答案
核心策略:行级状态机 + pending 缓冲 + 惰性渲染。
表格结束信号:收到第一个不以 | 开头的行(或流结束)。
🔧 Demo:最小可运行的流式 Markdown 渲染器
kotlin
// streaming-md.ts ------ npx ts-node streaming-md.ts
type Block = { type: 'text'; content: string } | { type: 'table'; lines: string[] };
class StreamingMarkdownParser {
private buffer: string[] = [];
private inTable = false;
private pending = '';
private blocks: Block[] = [];
feed(chunk: string) {
const lines = (this.pending + chunk).split('\n');
this.pending = lines.pop()!;
for (const line of lines) this.processLine(line);
}
private processLine(line: string) {
if (/^\s*|/.test(line)) {
this.inTable = true;
this.buffer.push(line);
} else {
if (this.inTable) this.flushTable();
if (line.trim()) this.blocks.push({ type: 'text', content: line });
}
}
private flushTable() {
const hasSep = this.buffer.some(l => /^|\s*[-:|]+\s*|/.test(l));
if (hasSep) {
this.blocks.push({ type: 'table', lines: [...this.buffer] });
} else {
// 不合法的表格结构,降级为普通文本
this.blocks.push(...this.buffer.map(l => ({ type: 'text' as const, content: l })));
}
this.buffer = [];
this.inTable = false;
}
finish(): Block[] {
if (this.pending) this.processLine(this.pending);
if (this.inTable) this.flushTable();
return this.blocks;
}
}
// 模拟 SSE 流式输入(每次 feed 一个 token)
const parser = new StreamingMarkdownParser();
const fakeStream = [
'普通文本\n',
'| 列A | 列B |\n',
'| --- | --- |\n',
'| 数据1 | 数', // ← 在行中间截断
'据2 |\n',
'\n', // ← 触发表格结束
'更多文本\n',
];
for (const chunk of fakeStream) {
parser.feed(chunk);
}
const blocks = parser.finish();
console.log(JSON.stringify(blocks, null, 2));
// 输出:[
// { type: 'text', content: '普通文本' },
// { type: 'table', lines: ['| 列A | 列B |', '| --- | --- |', '| 数据1 | 数据2 |'] },
// { type: 'text', content: '更多文本' }
// ]
💬 深追 Q&A 模拟
Q:如果 AI 的最后一个输出就是表格,没有后续文本,你的状态机会怎样?
这是个经典的 off-by-one 问题。流结束时
inTable还是true,buffer 里有数据但没有收到触发flushTable的非表格行。所以必须在流结束时强制调finish(),里面做最终的flushTable。EventSource 的close事件、fetch stream 的done === true,都要触发这个finish(),否则最后一个表格会永远留在 buffer 里不渲染。
Q:代码块(```````````包裹的内容)里如果有 | 字符,会不会被误判为表格行?
会!这是这个简化实现的缺陷。完整实现需要先判断当前是否在代码块内(tracking
inCodeBlock状态,遇到 ``````````` 切换),在代码块内的行跳过表格检测,直接缓冲。实际项目里我不会手写这些,会用marked或markdown-it的流式解析接口,它们已经处理了这些边界情况。
12. 前端 AI 开发中使用 SDD(Spec-Driven Development)有什么经验
核心答案
SDD = 先写规格(Spec),再让 AI 按规格实施。
核心流程:
需求理解 → 写 Spec(功能 + 边界 + 接口约定 + 不做什么)
→ Review Spec → AI 按 Spec 实施 → 对照 Spec 验收
关键经验: Spec 里"不做什么"比"做什么"更重要;Spec 即验收标准;Spec 是多 Agent 的上下文对齐工具。
一句话: SDD 把"和 AI 的沟通成本"转化成"写规格的成本",确定性需求划算,模糊探索不一定。
🔧 Demo:一个完整的 Spec 文档示例
php
<!-- specs/search-box.md -->
# SearchBox 组件 Spec
## 功能描述
一个带防抖的搜索输入框,用户输入后触发搜索回调。
## 接口约定
```ts
interface SearchBoxProps {
onSearch: (keyword: string) => void; // 搜索触发回调
placeholder?: string; // 默认:"搜索..."
debounceMs?: number; // 默认:300
disabled?: boolean;
}
行为规格
- 用户停止输入 300ms(可配置)后触发 onSearch
- keyword 为空字符串时不触发 onSearch(清空不搜索)
- 有内容时显示清空按钮(×),点击清空输入框并调用 onSearch('')
- disabled=true 时输入框灰色,不可交互,不触发回调
- 组件卸载时取消防抖定时器(防内存泄漏)
不做的事(Out of Scope)
- ❌ 不做搜索结果的展示(那是 SearchResults 组件的职责)
- ❌ 不做搜索历史
- ❌ 不处理 IME 输入法组合输入(中文拼音输入中不触发)
文件范围
只修改以下文件,不要改其他任何文件:
src/components/SearchBox/index.tsx(新建)src/components/SearchBox/index.module.css(新建)src/components/SearchBox/index.test.tsx(新建)
验收标准
对照上方行为规格逐条核对,所有 checkbox 通过即为完成。
使用方式:把 spec 传给 Claude Code
claude "请按照 specs/search-box.md 的规格实现 SearchBox 组件, 实现完成后对照规格里的 checkbox 逐条自检"
markdown
* * *
### 💬 深追 Q&A 模拟
**Q:Spec 写完 AI 实施,实施完发现 Spec 本身有问题,怎么办?**
> 这是 SDD 里最常见的问题。发现 Spec 有问题要先改 Spec,再让 AI 按新 Spec 重新实施,不要直接跟 AI 说"刚才那个需求变了"然后继续在同一个 session 里改,那样 Spec 和代码会越来越不对齐。改 Spec 的成本是值得的,因为 Spec 是后续验收和后续需求变更的基准,不维护它的代价更高。
**Q:需求比较模糊,写不出清楚的 Spec,还适合 SDD 吗?**
> 不适合。需求模糊时适合先做 spike(技术探索),跑一个 POC 搞清楚技术方向,这个阶段用"让 AI 先给我一个能跑的版本看看效果"的方式更高效。POC 做完、方向确定了,再写 Spec 进入 SDD 流程。SDD 适合的是"我知道要做什么,但实施起来繁琐"的场景,不适合"我不知道要做什么"的探索阶段。
**Q:团队里有人不愿意写 Spec,说写 Spec 的时间不如直接让 AI 做,你怎么说服他?**
> 我会先认可他的观点------需求简单时确实如此,不用为了 SDD 而 SDD。但如果一个功能要来回改 3 次,每次改完 AI 理解偏差,那 3 次的成本早就超过写一次 Spec 的成本了。更重要的是,Spec 不只是给 AI 看的,它也是和产品、设计、后端对齐的工具。如果写完 Spec 大家都没有异议,说明理解是对齐的;如果写完发现有分歧,早发现早解决,比代码写完再返工要便宜得多。
* * *
* * *
# Coding Agent 项目深追问题
* * *
## 13. Claude Code 和 Codex 各自有什么特别之处
**答:**
| 维度 | Claude Code | Codex(OpenAI) |
| ------ | ----------------------------- | ----------------------- |
| 接入方式 | CLI 工具,终端直接用 | API / GitHub Copilot 后端 |
| 上下文管理 | CLAUDE.md 自动注入、hooks、skill 系统 | 主要靠 system prompt |
| 指令跟随 | 精准,明确约束时很少越界 | 灵活,有时会"顺手"扩展 |
| 代码审查深度 | 执行为主,方案实施快 | 推理深度高,批判性更强 |
| 工具生态 | MCP 协议,可扩展工具 | Function Calling,生态更广 |
| 多文件操作 | 原生支持,能跨文件追踪依赖 | 需要手动控制文件范围 |
| 适合场景 | 主力实施、复杂多文件任务 | 独立 Review、批判性分析 |
**核心差异:** Claude Code 是"执行型",快但思考浅;Codex 是"审查型",慢但深。两者组合------Claude Code 做方案,Codex 审查------形成制衡。
### 💬 深追 Q&A
**Q:你说 Codex 推理深度更高,能举个具体例子体现这个差异吗?**
> 有一次让两个模型都 review 同一个并发控制的代码。Claude Code 给的 review 主要是风格和命名问题。Codex 发现了一个 race condition:两个异步操作都读了同一个状态变量,在特定时序下会出现数据不一致。这种需要跨多个执行路径推理的问题,Codex 更容易抓到,因为它更倾向于深度分析而不是快速响应。
* * *
## 14. 输入到模型的 Prompt 由哪些部分组成,哪些必须注入,哪些不是
**答:**
一个完整的 LLM prompt 通常由以下层次组成:
┌─────────────────────────────────────────┐ │ System Prompt(系统层) │ ← 必须,定义角色/能力/约束 ├─────────────────────────────────────────┤ │ Long-term Memory(长期记忆) │ ← 按需,用户偏好/项目规范 ├─────────────────────────────────────────┤ │ Skill / Tool Definitions(工具定义) │ ← 按需,当前任务需要的工具 ├─────────────────────────────────────────┤ │ Conversation History(对话历史) │ ← 按需,可压缩 ├─────────────────────────────────────────┤ │ Retrieved Context(RAG 召回内容) │ ← 按需,相关文档片段 ├─────────────────────────────────────────┤ │ User Message(用户当前输入) │ ← 必须 └─────────────────────────────────────────┘
markdown
**必须注入:**
- **System Prompt**:定义模型行为边界,没有它模型容易漂移
- **User Message**:当前任务输入,没有就无从响应
**按需注入(动态决策):**
- **长期记忆**:只在记忆与当前任务相关时注入,避免无关信息干扰
- **工具定义**:当前任务需要哪些工具就注入哪些,不全量注入(减少 token 消耗,降低幻觉)
- **对话历史**:可压缩,超出阈值时摘要替换原始对话
- **RAG 召回**:根据当前 query 相似度检索,不全量注入知识库
**设计原则:** 最小必要上下文(Minimum Necessary Context),注入越精准,输出质量越高,token 成本越低。
### 💬 深追 Q&A
**Q:工具定义为什么不全量注入,只注入当前需要的?**
> 两个原因。第一是 token 成本,一个工具定义可能有几百 token,十几个工具就是几千 token,每次调用都浪费。第二是模型选择工具时的"注意力"问题------工具越多,模型选错工具的概率越高,叫做 tool selection confusion。只注入当前任务真正需要的工具,模型能更准确地选择。
**Q:长期记忆按需注入,你怎么判断"按需",用什么方法决定要不要注入?**
> 语义相似度检索。把长期记忆按条目向量化存储,每次新任务来时,把用户消息做 embedding,和记忆库做 cosine similarity 检索,超过阈值(比如 0.75)的记忆条目才注入。这样既避免全量注入,又能在相关时自动召回。
* * *
## 15. 你做的 Coding Agent 和 Claude Code / Codex 的区别在哪
**答:** (根据自身项目情况替换,以下是参考框架)
| 维度 | Claude Code / Codex | 你的 Coding Agent |
| ---------- | -------------------- | -------------------- |
| 定制化程度 | 通用,需要靠 prompt 定制 | 针对特定场景/团队规范深度定制 |
| 记忆系统 | 基本无持久化记忆 | 长短期记忆分层,跨会话持久化 |
| Skill 体系 | 靠 slash command / 约定 | 自定义 skill 分层,自动匹配 |
| 上下文管理 | CLAUDE.md 等静态注入 | 动态 prompt,按任务类型组装 |
| 多 Agent 编排 | 单 Agent 为主 | 多 Agent 流水线,任务分发 |
| 成本控制 | 无精细控制 | token budget 管理,压缩策略 |
| 知识集成 | 靠文件上下文 | RAG 集成,知识库动态召回 |
**你的 Agent 的核心价值:** 深度集成团队/个人工作流,有持久化的上下文和记忆,能跨会话保持一致性,Claude Code 每次对话是全新的,不记得上次做了什么。
### 💬 深追 Q&A
**Q:你自己做的 Agent 和直接用 Claude Code 相比,日常使用哪个更多?**
> 实际上两个都在用,场景不同。Claude Code 我用来做临时性的、一次性的任务,比如快速重构一个函数、生成一个测试文件;我自己的 Agent 用来做需要跨会话积累上下文的长期任务,比如一个长达两周的功能开发,它记得之前的决策和规范,不用每次重新介绍项目背景。
* * *
## 16. 上下文压缩怎么做的,三层压缩策略是什么
**答:**
上下文压缩的目标是在有限的 token 窗口内保留最关键的信息。三层策略:
**第一层:消息级别裁剪(轻量)**
- 触发条件:历史消息条数超过阈值(如 20 条)
- 做法:滑动窗口,只保留最近 N 条对话,丢弃最早的
- 保留:System prompt 永远不裁剪;最近的 user-assistant 对
**第二层:摘要压缩(中量)**
- 触发条件:总 token 超过上下文窗口的 60%
- 做法:调用模型将前半段对话压缩为摘要("前面我们做了 X,决定了 Y,当前状态是 Z")
- 摘要以 system message 形式注入,替换原始对话
**第三层:关键事实提取(重量)**
- 触发条件:长期会话(超过 N 小时或 M 轮对话)
- 做法:提取关键决策、代码变更、约定事项存入长期记忆,完全清空短期对话历史
- 下次对话从长期记忆重建上下文
Token 使用率: ░░░░░░░░░░ 0% ────────── 60% → 触发第二层摘要 ──────────── 80% → 触发第三层提取 ─────────────── 100% → 截断(最差情况)
markdown
### 💬 深追 Q&A
**Q:为什么是 60% 这个阈值,不是 80% 或者满了再压缩?**
> 两个原因。第一,压缩本身需要消耗 token(要调用模型生成摘要),如果等到 90% 再压缩,压缩后剩余空间可能不够继续对话。第二,摘要质量和剩余 context 量有关,context 越满,模型处理时注意力越分散,摘要质量越差。60% 是经验值,留出足够的"呼吸空间"给压缩操作本身。
**Q:摘要是用同一个主模型生成,还是用轻量模型?**
> 摘要生成用轻量模型(如 Claude Haiku 或 GPT-4o-mini),原因是:① 摘要任务不需要强推理能力,轻量模型够用 ② 成本差异很大,Haiku 比 Sonnet 便宜 10 倍以上 ③ 速度快,不阻塞主流程。主模型只做核心任务,摘要是辅助操作。
* * *
## 17. 压缩过度导致效果不理想,怎么发现,怎么处理
**答:**
**如何发现压缩过度:**
1. **输出质量监控**:维护一个质量评分机制,比较压缩前后相同任务的输出质量(可以用另一个模型做评判)
1. **用户反馈信号**:用户频繁说"你刚才说的......你忘了"、"这个之前已经决定了"------说明关键信息被压掉了
1. **上下文一致性检查**:每轮对话后让模型输出"当前任务状态摘要",和上一轮比较是否有信息丢失
1. **明显错误**:模型开始重复已完成的工作,或者忽略已知约束
**处理策略:**
发现压缩过度 ├── 短期:从长期记忆恢复关键事实,重新注入 ├── 中期:降低压缩激进程度(提高触发阈值,增加保留信息量) └── 长期:改善关键事实提取算法,确保重要决策不被裁剪
// 压缩质量检查(伪代码) async function checkCompressionQuality( originalCtx: string, compressedCtx: string ): Promise { const score = await model.evaluate(` 原始上下文: originalCtx压缩后:{compressedCtx}
markdown
评估压缩质量(0-10),重点检查:
1. 关键决策是否保留
2. 重要约束是否保留
3. 当前任务状态是否准确
只输出分数
`); return parseFloat(score); }
markdown
* * *
## 18. Agent 对原有任务进一步修改(新加功能),系统怎么做
**答:**
这本质是"会话恢复 + 增量注入"的问题。
**流程:**
用户发起新修改请求 ↓ 从长期记忆检索相关上下文 (上次的任务描述、已完成内容、当前代码状态) ↓ 重建 prompt:
- System prompt(不变)
- 长期记忆摘要(已完成 X,当前状态 Y)
- 相关代码文件(当前版本,非历史版本)
- 新的修改请求(明确说明是在现有基础上修改) ↓ 执行,完成后更新长期记忆
markdown
**关键:需要重新注入的信息**
- ✅ 已完成功能的摘要(让模型知道不用重做)
- ✅ 当前代码文件(最新版本,不是历史对话里的版本)
- ✅ 已有的约束和规范(防止新功能违反旧约定)
- ❌ 不需要注入:历史的调试过程、已解决的错误、被废弃的方案
### 💬 深追 Q&A
**Q:新功能和旧功能有依赖关系,比如要改旧功能的接口来支持新功能,系统怎么处理?**
> 这是最复杂的情况。系统需要在 prompt 里明确说明变更点:"现有接口 X 需要从 A 改成 B,以下是受影响的调用方:[列表],请一并修改。"关键是要用工具扫描整个代码库找到所有调用方,而不是靠模型记忆------模型对代码的记忆是不可靠的,必须实时读文件。这也是为什么 Agent 要有文件系统访问工具,而不只是对话。
* * *
## 19. 工具调用的流程,skill 能替代工具吗
**答:**
**工具调用流程(Function Calling):**
用户输入 ↓ 模型生成响应(包含 tool_use 块): { "name": "read_file", "input": { "path": "src/api.ts" } } ↓ 系统执行工具,拿到结果 ↓ 把工具结果作为 tool_result 注入下一轮 ↓ 模型基于结果继续生成(可能再次调用工具) ↓ 模型输出最终文本响应(无 tool_use,循环结束)
markdown
**Skill 能替代工具吗?** 不能完全替代,两者本质不同:
| 维度 | 工具(Tool) | Skill |
| ---- | ------------------ | ---------------------- |
| 本质 | 代码执行,有实际副作用 | Prompt 模板,纯文本注入 |
| 能力 | 读文件、写文件、调 API、运行命令 | 提供任务指导、规范约束、思维框架 |
| 副作用 | 有(改变文件系统、调用外部服务) | 无(只影响模型的思考方向) |
| 替代关系 | 工具能做 skill 做不到的事 | Skill 能减少重复的 prompt 编写 |
**结论:** Skill 是工具的补充,不是替代。需要和外部系统交互的能力(读写文件、执行命令、调用 API)必须是工具;而高频的 prompt 模式、规范注入可以用 skill 封装。
* * *
## 20. 面向复杂任务,Coding Agent 的 Plan 怎么做
**答:**
复杂任务 Planning 的核心是把"一个大问题"拆成"多个可执行的子任务",并确定依赖关系。
**Planning 流程:**
接收复杂任务 ↓ Phase 1 - 任务理解(Clarification) 让模型列出不清楚的地方,用户确认后再规划 ↓ Phase 2 - 任务分解(Decomposition) 输出结构化 Plan: { "tasks": { "id": "T1", "description": "...", "files": \[..., "deps": \[\] }, { "id": "T2", "description": "...", "files": ..., "deps": "T1" }, { "id": "T3", "description": "...", "files": ..., "deps": \[\] }, // 可与 T2 并行 ] } ↓ Phase 3 - 执行编排(Orchestration) 按依赖关系并行/串行执行,无依赖的任务并行跑 ↓ Phase 4 - 集成验证(Integration) 子任务完成后,整体 review 接口一致性
markdown
### 💬 深追 Q&A
**Q:Plan 里的依赖关系怎么判断,模型能准确识别吗?**
> 模型判断依赖关系有时会漏,特别是隐式依赖(比如两个任务都要修改同一个共享类型定义文件)。我的做法是在模型生成 Plan 后,再用工具扫描每个子任务涉及的文件列表,自动检查文件重叠,重叠的任务标记为有依赖,不能并行。工具检查比模型判断更可靠。
* * *
## 21. 多 Agent 编排具体怎么做,每个子 Agent 的区别,为什么这么设计
**答:**
**编排架构:**
Orchestrator Agent(父) ├── 接收用户请求 ├── 生成 Plan,分发子任务 ├── 汇总子 Agent 结果 └── 处理冲突和集成
Worker Agents(子,按角色分工) ├── Implementer Agent ------ 写代码,只关注实现 ├── Reviewer Agent ------ 审查代码,批判性视角 ├── Tester Agent ------ 写测试,关注覆盖率和边界 └── Documenter Agent ------ 生成文档,关注可读性
markdown
**每个子 Agent 的区别:**
- **System prompt 不同**:Reviewer 的 system prompt 强调"批判性、找问题",Implementer 强调"精准执行、符合规范"
- **工具集不同**:Implementer 有写文件权限,Reviewer 只有读权限(防止 review 时顺手改代码)
- **上下文不同**:只给当前子任务相关的文件,不共享其他子 Agent 的工作过程
**为什么不让所有子 Agent 共享工具?**
> 权限最小化原则。Reviewer 不需要写文件,给了写权限反而引入风险------它可能在 review 时直接修改,绕过了 Implementer 的工作流。工具权限对应职责范围,职责越小、权限越小、出错面越小。
* * *
# Coding Agent 项目细节问题
* * *
## 22. Coding Agent 整个链路的运转流程
**答:** (参考框架,根据实际项目替换)
用户输入(自然语言任务描述) ↓ ① 意图理解 + Skill 匹配 ------ embedding 检索相关 skill,注入对应 prompt 规范 ↓ ② 长期记忆召回 ------ 向量检索,找和当前任务相关的历史记忆注入上下文 ↓ ③ 动态 Prompt 组装 ------ System prompt + 长期记忆 + skill + 对话历史(压缩后)+ 当前任务 ↓ ④ 模型推理(Plan 阶段) ------ 输出结构化执行计划(子任务列表 + 依赖关系) ↓ ⑤ 工具执行循环(ReAct 模式) ------ 读文件 → 分析 → 写文件 → 验证 → 循环直到完成 ↓ ⑥ 子任务完成,汇报结果 ↓ ⑦ 更新长期记忆(提取关键决策、变更摘要) ↓ ⑧ 返回用户,等待下一轮输入
markdown
* * *
## 23. Skill 分层体系怎么设计,为什么这么设计
**答:**
三层结构:
Layer 3 ------ 领域 Skill(最具体) 例:react-component、api-design、test-writing 触发:特定任务类型
Layer 2 ------ 规范 Skill(中间层) 例:code-style、commit-convention、pr-format 触发:每次写代码/提交时自动注入
Layer 1 ------ 基础 Skill(最通用) 例:project-context、team-preference 触发:每次对话都注入
markdown
**为什么分层:**
- 避免无效注入:不是每个任务都需要所有 skill,按需注入节省 token
- 灵活组合:一个复杂任务可以同时触发多层 skill(基础 + 规范 + 领域)
- 独立维护:各层 skill 可以独立更新,不互相影响
* * *
## 24. 用户输入怎么和相关 Skill 匹配
**答:**
语义匹配流程:
用户输入 ↓ 文本 embedding(向量化) ↓ 和 skill registry 里每个 skill 的描述向量做 cosine similarity ↓ 超过阈值(如 0.7)的 skill 候选列表 ↓ 按分数排序,取 Top K(如 Top 3) ↓ 注入匹配到的 skill 内容
markdown
**辅助策略:**
- 关键词触发:`/review` 命令直接触发 review skill,不走语义匹配
- 强制注入:基础层 skill 不走匹配,每次都注入
- 上下文感知:当前对话里已经触发过的 skill,后续轮次维持注入(避免反复检索)
* * *
## 25. Skill 沉淀机制
**答:**
Skill 不只是手动创建,还可以从对话中提炼沉淀:
**自动沉淀触发条件:**
- 用户对某个 AI 输出明确表示"很好,以后都这样做"
- 同一类 prompt 被重复使用超过 N 次
- 用户明确说"把这个存成 skill"
**沉淀流程:**
识别沉淀时机 ↓ 提取高频/高质量 prompt 模式 ↓ 让模型自动生成 skill 文档(name/description/content) ↓ 用户确认(or 自动保存) ↓ 写入 skill 文件,热更新到 registry
markdown
这样 skill 库是随使用不断增长的,而不是一次性手工配置。
* * *
## 26. 长短期记忆怎么设计,静态 vs 动态长期记忆
**答:**
**记忆分层:**
短期记忆(Short-term) ------ 当前对话的消息历史 ------ 存在内存,会话结束后消失 ------ 超出 token 阈值时触发压缩
长期记忆(Long-term) ------ 跨会话持久化,存在向量数据库 ------ 分为静态和动态两种
markdown
**静态长期记忆 vs 动态长期记忆:**
| 维度 | 静态长期记忆 | 动态长期记忆 |
| ---- | --------------------- | ------------------------------------------ |
| 内容 | 用户偏好、项目规范、固定约定 | 每次对话后提取的新事实 |
| 更新频率 | 低,手动维护 | 高,每轮对话自动更新 |
| 例子 | "用 TypeScript,不用 any" | "用户在 2026-06-01 决定把状态管理从 Redux 换成 Zustand" |
| 作用 | 提供稳定的背景规范 | 记录动态的决策和进展 |
**为什么要区分两种:**
静态的东西如果每次对话都更新,会引入噪声(AI 可能会"覆盖"掉用户设定的规范)。动态的东西如果不自动积累,用户每次都要重新介绍背景。两种分开管理,各自有不同的写入策略和召回逻辑。
* * *
## 27. 长期记忆快速积累过多,怎么处理
**答:**
**预防策略:**
- 设置记忆条目上限(如 1000 条),超出时触发整理
- 去重:相似度 > 0.9 的记忆条目合并
- 设置记忆有效期(TTL),超过 N 天未被召回的记忆降低权重或归档
**定期整理(Memory Consolidation):**
触发:记忆条目超过阈值 or 定时(每周) ↓ 把所有记忆条目聚类(语义聚类) ↓ 同一簇内的记忆合并为一条摘要记忆 ↓ 原始记忆条目降级为"归档",不参与日常召回 ↓ 只保留摘要记忆作为活跃记忆
markdown
这类似于人类记忆的"固化"过程------细节遗忘,关键事实保留。
* * *
## 28. 大模型怎么决定长期记忆是否召回
**答:**
两阶段:
**阶段一:向量检索(粗召回)**
把用户当前输入做 embedding,和记忆库做 cosine similarity 检索,返回 Top-K 最相关的记忆候选。
**阶段二:模型重排(精召回)**
把候选记忆和当前 query 一起发给模型,让模型判断哪些记忆真正有用:
以下是关于当前任务可能相关的记忆(按相似度排序): 记忆列表
请判断哪些记忆对当前任务"用户想实现搜索功能"有帮助, 输出相关记忆的 ID 列表,不相关的忽略。
markdown
**为什么需要两阶段:** 纯向量检索基于语义相似度,有时会召回"看起来相似但实际无关"的记忆。让模型做最终判断,结合语义理解决定真正的相关性,准确率更高。
* * *
## 29. 动态 Prompt vs 静态 Prompt
**答:**
**静态 Prompt:**
- 内容固定,每次对话都一样
- 例子:System Prompt 里的角色定义、基础规范
- 特点:稳定、可预测、不受运行时状态影响
**动态 Prompt:**
- 内容在运行时根据上下文动态组装
- 组成部分按条件注入/不注入
- 例子:
function buildPrompt(context: Context): string { const parts = SYSTEM_BASE, // 静态,永远注入 context.longTermMemory.join('\\n'), // 动态,按相关性注入 context.relevantSkills.map(s =\> s.content).join('\\n'), // 动态,按任务匹配 compressHistory(context.messages), // 动态,超阈值时压缩 context.ragResults ?? '', // 动态,有检索结果才注入 `用户当前任务:${context.userInput}`, // 动态,每次不同 ; return parts.filter(Boolean).join('\n\n'); }
markdown
**设计原则:** 静态部分保持稳定,动态部分最小化注入(只注入当前任务真正需要的信息),两者分开管理便于调试和迭代。
* * *
## 30. 模型底座选型,Token 消耗和成本
**答:** (参考框架,根据实际情况填数字)
**模型选型考虑:**
| 任务 | 推荐模型 | 理由 |
| ------------ | -------------------------- | ----------- |
| 主力实施(复杂代码任务) | Claude Sonnet / GPT-4o | 推理强,指令跟随好 |
| 摘要/压缩(辅助任务) | Claude Haiku / GPT-4o-mini | 成本低 10 倍,够用 |
| Embedding | text-embedding-3-small | 向量质量好,成本低 |
| 轻量判断(路由/分类) | GPT-4o-mini / Haiku | 快速、便宜 |
**Token 消耗估算:**
写 1000 行代码的任务:
- 输入 token(prompt + 上下文 + 工具结果):约 50k--100k token
- 输出 token(代码 + 解释):约 5k--20k token
- 总计:约 70k--120k token/任务
**成本估算(以 Claude Sonnet 为例):**
- 输入:$3/1M token
- 输出:$15/1M token
- 一个复杂任务:约 $0.3--0.8
**为什么成本这么高:** 主要是上下文注入------每轮对话都要带完整的 skill、记忆、历史,这些背景信息的 token 累加起来远超代码本身。优化方向是精细化上下文管理,只注入真正需要的部分。
* * *
# RAG 项目深追问题
* * *
## 31. RAG 的数据来源是什么
**答:** (根据实际项目替换,以下是参考框架)
数据来源通常分几类:
| 来源类型 | 处理方式 | 注意点 |
| --------------- | -------------------- | ------------------- |
| 内部文档(PDF/Word) | 解析 → 分块 → 向量化 | 格式复杂,需要处理表格/图片 |
| 网页 / Confluence | 爬取 → HTML 清洗 → 分块 | 去除导航、广告、重复内容 |
| 代码库 | AST 解析或文本分块 | 按函数/类切分比按行数切分好 |
| 数据库结构化数据 | 转为自然语言描述再向量化 | 需要 schema 感知的描述生成 |
| API 文档 | OpenAPI / Swagger 解析 | 按 endpoint 切分,带参数说明 |
**数据质量是 RAG 效果的上限**,垃圾进垃圾出,入库前的清洗比模型选型更重要。
* * *
## 32. 个人为什么要做 GraphRAG 项目
**答:** (参考思路)
可以从"发现了什么问题 → 现有方案为什么不够 → GraphRAG 怎么解决"来组织:
> 我在使用普通 RAG 系统时,发现对于需要跨文档推理的问题,回答质量很差。比如"某个技术决策的背景和影响"这类问题,涉及多个文档的关联信息,单纯向量检索找不到全图,回答是片面的。纯向量 RAG 擅长"找到相关段落",但不擅长"理解段落之间的关系"。GraphRAG 通过构建知识图谱,让检索有了图结构的多跳能力,能回答"A 和 B 有什么关系"这类需要推理的问题。这是我做这个项目的核心出发点。
* * *
## 22. GraphRAG 和纯向量检索的区别,解决了什么问题
**答:**
**纯向量检索的局限:**
- 每个文档 chunk 独立向量化,检索时基于语义相似度
- 擅长"这段话在哪里"(单跳检索)
- 无法处理需要跨多个文档/段落推理的问题(多跳推理)
**GraphRAG 的做法:**
- 在向量检索基础上,构建知识图谱:实体(Entity)、关系(Relation)、社区(Community)
- 检索时不只找相似 chunk,还沿知识图谱的边做多跳遍历
**解决的问题:**
| 问题类型 | 纯向量 | GraphRAG |
| ----------------- | --- | -------- |
| 直接问答(某概念是什么) | ✅ | ✅ |
| 多跳推理(A 和 B 有什么关系) | ❌ | ✅ |
| 全局概括(整个文档库的主题) | ❌ | ✅(社区摘要) |
| 推理型问题(根据多个事实推结论) | ❌ | ✅ |
**什么算推理型问题(举例):**
知识库里有:
- 文档A:张三是部门A的负责人
- 文档B:部门A负责项目X
- 文档C:项目X使用了技术Y
推理型问题:张三对技术Y的熟悉程度如何影响项目X? → 需要跨 A/B/C 三个文档推理,纯向量检索很难做到
markdown
### 💬 深追 Q&A
**Q:GraphRAG 的知识图谱构建成本很高,你怎么控制这个成本?**
> 两个策略。第一,不是所有内容都建图,只对核心知识(实体丰富、关系复杂的文档)建图,普通问答还是走向量检索。第二,图谱构建是离线异步的,入库时在后台跑,不影响实时响应。实体和关系抽取用轻量模型(Haiku/mini),只有社区摘要生成用强模型,控制成本。
**Q:Graph 里的关系是怎么抽取的,准确率能到多少?**
> LLM 做关系抽取,给定实体对和上下文,让模型判断关系类型和方向。准确率大概在 80-85% 左右,有噪声。关键是要有去噪机制:同一关系出现多次(多个文档都提到)才确认入图,出现一次的可能是误抽取,不入图或者降低置信度。
* * *
## 23. PDF 中各种形式的内容怎么处理
**答:**
PDF 的内容形式:
| 内容类型 | 处理方式 |
| ----------- | -------------------------------------------------- |
| 纯文本 | `pdfplumber` / `pymupdf` 直接提取 |
| 表格 | `pdfplumber` 提取表格结构,转为 Markdown / CSV |
| 图片 | 用视觉模型(GPT-4o / Claude)做图片描述(Image Caption) |
| 公式 | MathPix API 或 `nougat`(PDF 学术公式专用模型)转 LaTeX |
| 扫描件(图片 PDF) | OCR(Tesseract / PaddleOCR / Azure Form Recognizer) |
| 混合排版(多栏) | `pdfplumber` 按坐标排序,还原阅读顺序 |
**处理流程:**
PDF 输入 → 判断是否是扫描件(有无可提取文字层) → 普通 PDF:pymupdf 提取文字 + 表格 → 扫描件:OCR 转文字 → 检测图片块 → 视觉模型生成描述 → 检测表格 → 转结构化格式 → 分块(chunk)→ 向量化 → 入库
markdown
* * *
## 24. 数据冗余和无关信息的入库前处理
**答:**
**清洗策略(Pipeline):**
1. **去重**:文档级 MD5 hash 去重,避免同一文档入库多次;chunk 级语义去重(cosine similarity > 0.95 的 chunk 只保留一个)
1. **噪声过滤**:移除页眉页脚(通常在固定坐标)、水印文字、导航菜单文本
1. **质量过滤**:过短的 chunk(< 50 字)、乱码检测(非法字符比例 > X%)直接丢弃
1. **相关性过滤**:用轻量分类模型判断 chunk 是否属于目标知识域,无关的不入库
1. **结构保留**:保留标题层级(H1/H2/H3),chunk 时带上标题作为上下文前缀
* * *
## 25. 知识抽取怎么做
**答:**
知识抽取 = 从非结构化文本里提取结构化知识(实体、关系、属性)。
**流程:**
文档 chunk → 命名实体识别(NER):识别人名/地名/组织/产品/概念 → 关系抽取(RE):判断实体间的关系类型和方向 → 属性抽取:提取实体的属性值("张三 的 职位 是 CTO") → 知识融合:同一实体的不同表述合并("AI" = "人工智能") → 写入知识图谱
markdown
**实现方式:**
- 小规模:LLM 直接做(prompt 里指定抽取格式,输出 JSON)
- 大规模:先用轻量 NER 模型识别实体,再用 LLM 做关系判断(降低成本)
* * *
## 26. 增量更新怎么做
**答:**
**核心问题:** 新文档入库不能触发全量重新索引,否则成本爆炸。
**策略:**
新文档到来 ↓ Hash 比对:新文档 vs 已入库文档 ├── 完全相同 → 跳过 ├── 新文档(无记录)→ 正常处理流程入库 └── 已有文档更新 → 差量更新: 只对变更的段落重新向量化 删除旧版 chunk,插入新版 chunk 更新知识图谱中受影响的节点和边
markdown
**实现要点:**
- 每个 chunk 记录来源文档 ID + 文档版本号
- 删除某文档时,按文档 ID 批量删除所有 chunk
- 知识图谱里的节点带 source_doc_id,更新文档时同步更新相关节点
* * *
## 27. 文档冲突处理,版本控制,错误文档回滚
**答:**
**文档冲突处理:**
检测到冲突(同一事实在不同文档有不同描述) ↓ 记录冲突,不自动解决 ↓ 检索时:
- 把冲突信息一并返回给模型
- 让模型在回答时注明"来源 A 说 X,来源 B 说 Y,存在冲突"
- 或按文档权重/时间戳优先(新文档 > 旧文档)
markdown
**版本控制:**
- 每个文档入库时记录:文档 ID、版本号、入库时间、操作人
- 保留最近 N 个版本的快照(向量 + 原文)
- 新版本入库不立即删除旧版本,有验证期(如 24 小时)后再清理
**错误文档回滚:**
概念性命令
knowledge-base rollback
--doc-id "doc_xxx"
--to-version "v2" # 回滚到指定版本
底层操作:
1. 删除当前版本的所有 chunk(按 doc_id + version)
2. 恢复旧版本的 chunk 到向量数据库
3. 更新知识图谱(删除新版节点,恢复旧版节点)
4. 记录回滚操作日志
markdown
* * *
# Fine-tuning 项目问题
* * *
## 28. 为什么要做 Fine-tuning 项目,出发点是什么
**答:** (参考框架,根据实际项目替换)
可以从"API 调用的局限 → Fine-tuning 能解决什么 → 个人学习价值"三层来回答:
> 出发点有两个。第一是工程层面:直接调用闭源 API 有数据隐私问题(代码/内部文档发给第三方)、延迟不可控、成本随用量线性增长,Fine-tuning 开源模型可以本地部署,解决这些问题。第二是技术学习层面:理解模型训练过程,对做 Agent、做 RAG、做 prompt 工程都有帮助,知道模型"为什么这样",而不只是知道"怎么用"。选 Llama3 是因为它在开源模型里综合能力最强,社区工具链(Unsloth、LLaMA-Factory)完善,上手成本低。
* * *
## 29. SFT、DPO、GRPO 分别是什么,区别在哪
**答:**
**SFT(Supervised Fine-Tuning,监督微调)**
用标注好的(问题, 回答)对直接训练模型,最大化正确答案的对数概率。
训练数据:(prompt, chosen_response) 对 目标:让模型学会在给定 prompt 时生成 chosen_response 这样的输出 优点:简单直接,数据易获取 缺点:只学"正确是什么",不学"为什么错的不好"
markdown
**DPO(Direct Preference Optimization,直接偏好优化)**
用偏好对(同一 prompt 下,好回答 vs 差回答)训练,让模型增大好回答概率、降低差回答概率。
训练数据:(prompt, chosen_response, rejected_response) 三元组 目标:让 chosen 和 rejected 的概率差距最大化 优点:比 RLHF 更稳定(不需要单独训练 reward model) 缺点:需要人工标注偏好数据
markdown
**GRPO(Group Relative Policy Optimization)**
对同一 prompt 采样多个响应,以组内相对质量作为奖励信号,不需要参考模型。DeepSeek-R1 训练使用了这个方法。
训练数据:(prompt, response_1, response_2, ..., response_n) 奖励:每个 response 相对于组内平均质量的得分 优点:不需要 reference model,内存效率更高;天然适合可验证奖励(数学/代码) 缺点:奖励函数设计复杂,不可验证任务难以应用
markdown
| 维度 | SFT | DPO | GRPO |
| --------------- | ------------------ | -------------------------- | ----------------------- |
| 数据形式 | (prompt, response) | (prompt, chosen, rejected) | (prompt, [多个responses]) |
| 需要 Reward Model | 否 | 否 | 否(内置奖励函数) |
| 训练稳定性 | 最高 | 高 | 中 |
| 适合场景 | 格式对齐、知识注入 | 风格偏好、安全对齐 | 推理、数学、代码(可验证任务) |
* * *
## 29. 为什么基于 Llama3 架构,数据集大小,评测指标
**答:** (参考框架,根据实际项目替换)
**为什么选 Llama3:**
- 开源,权重可商用(Llama3 Community License)
- 架构成熟,社区支持好(Unsloth、LLaMA-Factory 等训练框架完善)
- 在同等参数量下,Llama3 性能在开源模型中处于前列
- 中文支持:Llama3 词表中包含中文 token(比 Llama2 好很多)
**数据集大小参考:**
- SFT:通常 1k--100k 条,高质量 > 低质量大量数据
- DPO:几千到几万条偏好对
- 特定领域:500--5000 条高质量领域数据足以明显改变模型行为
**常用评测指标:**
| 任务类型 | 指标 |
| ---- | --------------------- |
| 代码生成 | HumanEval Pass@1、MBPP |
| 数学推理 | GSM8K、MATH |
| 中文理解 | C-Eval、CMMLU |
| 对话质量 | MT-Bench(GPT-4 评判) |
| 领域专项 | 自建测试集,人工评分 |
* * *
# 其他问题
* * *
## 30. 相比别人的优势是什么
**参考思路(根据实际情况组织):**
从三个层面回答:
**深度实践层面:** 不只是会用 AI 工具,而是深度参与了从工具使用到系统构建的全链路------自建 Coding Agent、GraphRAG、做过 fine-tuning,有端到端的工程经验,而不只停留在 prompt 工程层面。
**工程化思维层面:** 把 AI 能力工程化------skill 系统、上下文治理、多 Agent 编排、成本控制,这些都是把 AI 从"实验品"变成"生产可用系统"需要解决的工程问题,有实际落地经验。
**持续学习层面:** 跟踪最新进展(SDD、GRPO、GraphRAG),并能把新技术快速应用到实际项目,不只是了解概念。
* * *
## 31. 反问:AI 发展这么快,对业务最大的赋能点在哪
**参考答案:**
> 我认为最大的赋能点不在于模型本身有多强,而在于**AI 把知识工作的执行成本大幅压低**。一个高级工程师做架构决策需要 2 小时,AI 辅助后可能 30 分钟;一个产品经理写 PRD 需要一天,AI 辅助后可能半天。这种"执行成本下降"会让团队能够尝试更多方向,迭代更快。对业务来说,最大的赋能是**降低试错成本,提高迭代速度**,而不是让某一件事做得更好。
* * *
## 32. 反问:基模快速迭代,Agent 还有必要吗
**参考答案:**
> 我认为有,而且越来越必要。基模变强,解决的是"单次对话能做多复杂的事";Agent 解决的是"如何把多次交互、多个工具、多步流程组织成一个可靠的系统"。这是两个维度的问题。基模再强,也没法自动决定什么时候调用什么工具、怎么管理长期上下文、出错了怎么恢复------这些是 Agent 架构要解决的问题。实际上基模越强,Agent 能做的事就越复杂,两者是相互放大的关系,而不是替代关系。
* * *
*基于 2026 年 6 月技术现状整理,面试时用自身项目经验替换 Demo 里的示例效果更佳。*