返回笔记首页

20.3 AI 代码助手 - 完整实现方案

主题配置

一、技术架构设计

1.1 整体架构

plain
前端层(Vue3)
  ├── 代码编辑器
  ├── 提示面板
  └── 交互控制

AI 能力层
  ├── 代码补全(Claude API)
  ├── 代码解释(Claude API)
  └── 测试生成(Claude API)

代码分析层
  ├── 语法分析
  ├── 上下文提取
  └── 依赖检测

1.2 核心功能模块

  • 代码补全:智能预测和生成代码片段
  • 代码解释:自然语言解释代码逻辑
  • 单元测试生成:自动生成测试用例

二、代码补全实现

2.1 技术实现原理

核心思路

  1. 监听用户代码编辑行为
  2. 提取上下文(当前文件、光标位置、已有代码)
  3. 调用 AI 预测下一步代码
  4. 以浮层形式展示补全建议
  5. 用户可一键接受或继续编辑
关键技术点
  • 上下文提取:前后 20 行代码 + 函数签名 + 导入语句
  • 触发时机:特殊字符(. / ( { 等)或停顿 1 秒
  • 补全策略:单行补全 / 多行补全 / 整个函数生成

2.2 完整代码实现

vue
<template>
  <div class="code-completion-container">
    <h2>AI 代码补全助手</h2>

    <div class="editor-section">
      <div class="editor-header">
        <span class="file-name">main.js</span>
        <div class="editor-controls">
          <button @click="clearCode" class="btn-clear">清空</button>
          <button @click="runCode" class="btn-run">运行代码</button>
        </div>
      </div>

      <div class="editor-wrapper">
        <textarea
          ref="codeEditor"
          v-model="code"
          @input="handleCodeChange"
          @keydown="handleKeyDown"
          @click="updateCursorPosition"
          class="code-textarea"
          spellcheck="false"
          placeholder="开始输入代码,AI 会智能补全..."
        ></textarea>

        <!-- 行号 -->
        <div class="line-numbers">
          <div
            v-for="n in lineCount"
            :key="n"
            class="line-number"
          >
            {{ n }}
          </div>
        </div>

        <!-- 补全建议浮层 -->
        <div
          v-if="showCompletion && completion"
          class="completion-popup"
          :style="completionPosition"
        >
          <div class="completion-header">
            <span class="completion-icon">💡</span>
            <span class="completion-label">AI 建议</span>
            <span class="completion-shortcut">Tab 接受 / Esc 关闭</span>
          </div>
          <pre class="completion-code">{{ completion }}</pre>
          <div class="completion-footer">
            <button @click="acceptCompletion" class="btn-accept">接受</button>
            <button @click="closeCompletion" class="btn-reject">忽略</button>
          </div>
        </div>
      </div>

      <!-- 状态栏 -->
      <div class="editor-footer">
        <span class="footer-item">行: {{ currentLine }}</span>
        <span class="footer-item">列: {{ currentColumn }}</span>
        <span class="footer-item">字符: {{ code.length }}</span>
        <span v-if="isGenerating" class="footer-status">
          AI 生成中...
        </span>
      </div>
    </div>

    <!-- AI 生成历史 -->
    <div v-if="completionHistory.length > 0" class="history-section">
      <h3>补全历史</h3>
      <div
        v-for="(item, index) in completionHistory"
        :key="index"
        class="history-item"
      >
        <div class="history-header">
          <span class="history-time">{{ item.time }}</span>
          <span class="history-status" :class="item.accepted ? 'accepted' : 'rejected'">
            {{ item.accepted ? '已接受' : '已拒绝' }}
          </span>
        </div>
        <div class="history-context">触发上下文: {{ item.context }}</div>
        <pre class="history-code">{{ item.completion }}</pre>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, nextTick } from 'vue'

const codeEditor = ref(null)
const code = ref('')
const completion = ref('')
const showCompletion = ref(false)
const isGenerating = ref(false)
const currentLine = ref(1)
const currentColumn = ref(1)
const completionPosition = ref({ top: '0px', left: '0px' })
const completionHistory = ref([])

let completionTimer = null

// 计算行数
const lineCount = computed(() => {
  return code.value.split('\n').length
})

// 处理代码变化
const handleCodeChange = () => {
  updateCursorPosition()

  // 防抖处理
  clearTimeout(completionTimer)
  completionTimer = setTimeout(() => {
    triggerCompletion()
  }, 1000) // 停顿 1 秒后触发补全
}

// 处理键盘事件
const handleKeyDown = (event) => {
  // Tab 键接受补全
  if (event.key === 'Tab' && showCompletion.value) {
    event.preventDefault()
    acceptCompletion()
    return
  }

  // Esc 键关闭补全
  if (event.key === 'Escape' && showCompletion.value) {
    event.preventDefault()
    closeCompletion()
    return
  }

  // 特殊字符触发补全
  const triggerChars = ['.', '(', '{', '[', ' ']
  if (triggerChars.includes(event.key)) {
    clearTimeout(completionTimer)
    completionTimer = setTimeout(() => {
      triggerCompletion()
    }, 300)
  }
}

// 更新光标位置
const updateCursorPosition = () => {
  const textarea = codeEditor.value
  if (!textarea) return

  const cursorPos = textarea.selectionStart
  const textBeforeCursor = code.value.substring(0, cursorPos)
  const lines = textBeforeCursor.split('\n')

  currentLine.value = lines.length
  currentColumn.value = lines[lines.length - 1].length + 1

  // 更新补全浮层位置
  updateCompletionPosition()
}

// 更新补全浮层位置
const updateCompletionPosition = () => {
  const textarea = codeEditor.value
  if (!textarea) return

  const rect = textarea.getBoundingClientRect()
  const lineHeight = 20
  const charWidth = 8.4

  completionPosition.value = {
    top: `${(currentLine.value - 1) * lineHeight + 40}px`,
    left: `${currentColumn.value * charWidth + 50}px`
  }
}

// 触发代码补全
const triggerCompletion = async () => {
  if (isGenerating.value || !code.value.trim()) return

  isGenerating.value = true
  showCompletion.value = false

  try {
    // 提取上下文
    const context = extractContext()

    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'claude-sonnet-4-20250514',
        max_tokens: 1000,
        messages: [{
          role: 'user',
          content: `你是一个代码补全助手。根据用户当前的代码上下文,预测他接下来可能要写的代码。

当前代码:
\`\`\`javascript
${context.before}█${context.after}
\`\`\`

光标位置:█

请补全光标位置之后的代码。要求:
1. 只返回要补全的代码,不要重复已有代码
2. 代码要符合上下文逻辑
3. 保持代码风格一致
4. 如果是函数调用,补全参数;如果是对象访问,补全属性
5. 补全内容控制在 5-10 行

只返回代码,不要任何解释。不要用 \`\`\` 包裹。`
        }]
      })
    })

    const data = await response.json()
    const suggestedCode = data.content[0].text.trim()

    // 清理可能的代码块标记
    completion.value = suggestedCode
      .replace(/^```(?:javascript|js)?/gm, '')
      .replace(/```$/gm, '')
      .trim()

    if (completion.value) {
      showCompletion.value = true
      updateCompletionPosition()
    }
  } catch (error) {
    console.error('代码补全失败:', error)
  } finally {
    isGenerating.value = false
  }
}

// 提取代码上下文
const extractContext = () => {
  const textarea = codeEditor.value
  const cursorPos = textarea.selectionStart

  const before = code.value.substring(0, cursorPos)
  const after = code.value.substring(cursorPos)

  // 提取光标前后各 10 行
  const beforeLines = before.split('\n').slice(-10).join('\n')
  const afterLines = after.split('\n').slice(0, 10).join('\n')

  return {
    before: beforeLines,
    after: afterLines,
    cursor: cursorPos
  }
}

// 接受补全
const acceptCompletion = () => {
  const textarea = codeEditor.value
  const cursorPos = textarea.selectionStart

  const before = code.value.substring(0, cursorPos)
  const after = code.value.substring(cursorPos)

  code.value = before + completion.value + after

  // 记录到历史
  completionHistory.value.unshift({
    time: new Date().toLocaleTimeString(),
    context: before.split('\n').slice(-2).join('\n'),
    completion: completion.value,
    accepted: true
  })

  closeCompletion()

  // 移动光标到补全内容之后
  nextTick(() => {
    const newCursorPos = cursorPos + completion.value.length
    textarea.setSelectionRange(newCursorPos, newCursorPos)
    textarea.focus()
  })
}

// 关闭补全
const closeCompletion = () => {
  if (completion.value && showCompletion.value) {
    // 记录拒绝的补全
    completionHistory.value.unshift({
      time: new Date().toLocaleTimeString(),
      context: extractContext().before.split('\n').slice(-2).join('\n'),
      completion: completion.value,
      accepted: false
    })
  }

  showCompletion.value = false
  completion.value = ''
}

// 清空代码
const clearCode = () => {
  if (confirm('确定要清空代码吗?')) {
    code.value = ''
    closeCompletion()
  }
}

// 运行代码
const runCode = () => {
  try {
    eval(code.value)
    alert('代码执行成功!')
  } catch (error) {
    alert('代码执行错误:\n' + error.message)
  }
}

// 初始化示例代码
code.value = `// 示例:定义一个计算器类
class Calculator {
  constructor() {
    this.result = 0
  }

  // 试试输入 add 然后停顿,AI 会帮你补全
  `
</script>

<style scoped>
.code-completion-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 30px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

h2, h3 {
  color: #2c3e50;
  margin-bottom: 20px;
}

.editor-section {
  background: #1e1e1e;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  margin-bottom: 30px;
}

.editor-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 20px;
  background: #2d2d30;
  border-bottom: 1px solid #3e3e42;
}

.file-name {
  color: #cccccc;
  font-size: 14px;
  font-weight: 500;
}

.editor-controls {
  display: flex;
  gap: 10px;
}

.btn-clear,
.btn-run {
  padding: 6px 16px;
  border: none;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-clear {
  background: #3e3e42;
  color: #cccccc;
}

.btn-clear:hover {
  background: #4e4e52;
}

.btn-run {
  background: #0e639c;
  color: white;
}

.btn-run:hover {
  background: #1177bb;
}

.editor-wrapper {
  position: relative;
  display: flex;
}

.line-numbers {
  padding: 16px 0;
  background: #1e1e1e;
  border-right: 1px solid #3e3e42;
  user-select: none;
}

.line-number {
  height: 20px;
  line-height: 20px;
  padding: 0 16px;
  color: #858585;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 13px;
  text-align: right;
}

.code-textarea {
  flex: 1;
  min-height: 400px;
  padding: 16px;
  background: #1e1e1e;
  color: #d4d4d4;
  border: none;
  outline: none;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 14px;
  line-height: 20px;
  resize: none;
  tab-size: 2;
}

.code-textarea::placeholder {
  color: #6a9955;
}

.completion-popup {
  position: absolute;
  background: #252526;
  border: 1px solid #3e3e42;
  border-radius: 8px;
  padding: 12px;
  max-width: 500px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.3);
  z-index: 1000;
  animation: fadeIn 0.2s ease-in-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.completion-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid #3e3e42;
}

.completion-icon {
  font-size: 16px;
}

.completion-label {
  color: #4ec9b0;
  font-size: 13px;
  font-weight: 600;
}

.completion-shortcut {
  margin-left: auto;
  color: #858585;
  font-size: 11px;
}

.completion-code {
  color: #d4d4d4;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 13px;
  line-height: 18px;
  margin: 0;
  padding: 8px;
  background: #1e1e1e;
  border-radius: 4px;
  white-space: pre-wrap;
  word-wrap: break-word;
}

.completion-footer {
  display: flex;
  gap: 8px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid #3e3e42;
}

.btn-accept,
.btn-reject {
  flex: 1;
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-accept {
  background: #0e639c;
  color: white;
}

.btn-accept:hover {
  background: #1177bb;
}

.btn-reject {
  background: #3e3e42;
  color: #cccccc;
}

.btn-reject:hover {
  background: #4e4e52;
}

.editor-footer {
  display: flex;
  gap: 20px;
  padding: 8px 20px;
  background: #007acc;
  color: white;
  font-size: 12px;
}

.footer-item {
  color: white;
}

.footer-status {
  margin-left: auto;
  color: #ffffff;
  font-weight: 500;
}

.history-section {
  background: white;
  padding: 24px;
  border-radius: 12px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}

.history-item {
  padding: 16px;
  margin-bottom: 16px;
  background: #f5f7fa;
  border-radius: 8px;
  border-left: 3px solid #409eff;
}

.history-item:last-child {
  margin-bottom: 0;
}

.history-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.history-time {
  color: #909399;
  font-size: 12px;
}

.history-status {
  padding: 3px 8px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 500;
}

.history-status.accepted {
  background: #e1f3d8;
  color: #67c23a;
}

.history-status.rejected {
  background: #fde2e2;
  color: #f56c6c;
}

.history-context {
  color: #606266;
  font-size: 13px;
  margin-bottom: 8px;
  font-style: italic;
}

.history-code {
  color: #303133;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 13px;
  line-height: 18px;
  margin: 0;
  padding: 10px;
  background: white;
  border-radius: 4px;
  white-space: pre-wrap;
  word-wrap: break-word;
}
</style>

三、代码解释实现

3.1 技术实现原理

核心思路

  1. 用户选中代码片段
  2. 点击"解释代码"按钮
  3. AI 分析代码逻辑
  4. 用自然语言解释代码功能、参数、返回值等
  5. 展示解释结果
解释维度
  • 整体功能:这段代码做什么
  • 逻辑流程:代码执行步骤
  • 关键变量:重要变量的作用
  • 潜在问题:可能的 bug 或优化点

3.2 完整代码实现

vue
<template>
  <div class="code-explanation-container">
    <h2>AI 代码解释助手</h2>

    <div class="explanation-layout">
      <!-- 左侧:代码输入 -->
      <div class="code-panel">
        <div class="panel-header">
          <span>输入代码</span>
          <button @click="loadExample" class="btn-example">加载示例</button>
        </div>
        <textarea
          v-model="codeToExplain"
          placeholder="粘贴你想要理解的代码..."
          class="code-input"
          rows="20"
        ></textarea>
        <button
          @click="explainCode"
          class="btn-explain"
          :disabled="explaining || !codeToExplain.trim()"
        >
          {{ explaining ? '分析中...' : '解释这段代码' }}
        </button>
      </div>

      <!-- 右侧:解释结果 -->
      <div class="explanation-panel">
        <div class="panel-header">
          <span>AI 解释</span>
          <button
            v-if="explanation"
            @click="copyExplanation"
            class="btn-copy"
          >
            复制解释
          </button>
        </div>

        <div v-if="!explanation && !explaining" class="empty-state">
          <div class="empty-icon">📖</div>
          <div class="empty-text">在左侧输入代码,点击"解释"按钮</div>
        </div>

        <div v-if="explaining" class="loading-state">
          <div class="loader"></div>
          <div class="loading-text">AI 正在分析代码...</div>
        </div>

        <div v-if="explanation" class="explanation-content">
          <!-- 整体功能 -->
          <div class="section">
            <div class="section-title">整体功能</div>
            <div class="section-content">{{ explanation.overview }}</div>
          </div>

          <!-- 详细解释 -->
          <div class="section">
            <div class="section-title">详细解释</div>
            <div
              v-for="(step, index) in explanation.steps"
              :key="index"
              class="step-item"
            >
              <div class="step-number">{{ index + 1 }}</div>
              <div class="step-content">
                <div class="step-code">{{ step.code }}</div>
                <div class="step-desc">{{ step.description }}</div>
              </div>
            </div>
          </div>

          <!-- 关键概念 -->
          <div v-if="explanation.concepts" class="section">
            <div class="section-title">关键概念</div>
            <div class="concepts-list">
              <div
                v-for="(concept, index) in explanation.concepts"
                :key="index"
                class="concept-item"
              >
                <div class="concept-name">{{ concept.name }}</div>
                <div class="concept-desc">{{ concept.description }}</div>
              </div>
            </div>
          </div>

          <!-- 注意事项 -->
          <div v-if="explanation.notes" class="section">
            <div class="section-title">注意事项</div>
            <div class="notes-list">
              <div
                v-for="(note, index) in explanation.notes"
                :key="index"
                class="note-item"
              >
                <span class="note-icon">⚠️</span>
                <span class="note-text">{{ note }}</span>
              </div>
            </div>
          </div>

          <!-- 改进建议 -->
          <div v-if="explanation.improvements" class="section">
            <div class="section-title">改进建议</div>
            <div class="improvements-list">
              <div
                v-for="(improvement, index) in explanation.improvements"
                :key="index"
                class="improvement-item"
              >
                <div class="improvement-title">{{ improvement.title }}</div>
                <div class="improvement-desc">{{ improvement.description }}</div>
                <pre v-if="improvement.code" class="improvement-code">{{ improvement.code }}</pre>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const codeToExplain = ref('')
const explanation = ref(null)
const explaining = ref(false)

// 加载示例代码
const loadExample = () => {
  codeToExplain.value = `function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

const handleSearch = debounce((query) => {
  console.log('搜索:', query);
}, 500);`
}

// 解释代码
const explainCode = async () => {
  explaining.value = true
  explanation.value = null

  try {
    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'claude-sonnet-4-20250514',
        max_tokens: 2500,
        messages: [{
          role: 'user',
          content: `请详细解释这段代码:

\`\`\`javascript
${codeToExplain.value}
\`\`\`

请按以下结构返回 JSON:
{
  "overview": "这段代码的整体功能(1-2句话)",
  "steps": [
    {
      "code": "代码片段",
      "description": "这个步骤做什么"
    }
  ],
  "concepts": [
    {
      "name": "概念名称",
      "description": "概念解释"
    }
  ],
  "notes": [
    "注意事项1",
    "注意事项2"
  ],
  "improvements": [
    {
      "title": "改进建议标题",
      "description": "建议详情",
      "code": "改进后的代码(可选)"
    }
  ]
}

要求:
1. 解释要通俗易懂,适合初学者
2. 逐行或逐段解释代码逻辑
3. 指出关键概念和技术点
4. 如果有潜在问题或改进空间,要指出

只返回JSON,不要其他内容。`
        }]
      })
    })

    const data = await response.json()
    const content = data.content[0].text
    const jsonMatch = content.match(/\{[\s\S]*\}/)

    if (jsonMatch) {
      explanation.value = JSON.parse(jsonMatch[0])
    }
  } catch (error) {
    console.error('代码解释失败:', error)
    alert('解释失败,请重试')
  } finally {
    explaining.value = false
  }
}

// 复制解释
const copyExplanation = () => {
  let text = `整体功能:\n${explanation.value.overview}\n\n`

  text += `详细解释:\n`
  explanation.value.steps.forEach((step, i) => {
    text += `${i + 1}. ${step.code}\n   ${step.description}\n\n`
  })

  if (explanation.value.concepts) {
    text += `关键概念:\n`
    explanation.value.concepts.forEach(c => {
      text += `- ${c.name}: ${c.description}\n`
    })
    text += '\n'
  }

  if (explanation.value.notes) {
    text += `注意事项:\n`
    explanation.value.notes.forEach(n => {
      text += `- ${n}\n`
    })
  }

  navigator.clipboard.writeText(text)
  alert('已复制到剪贴板!')
}
</script>

<style scoped>
.code-explanation-container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 30px;
}

h2 {
  text-align: center;
  color: #2c3e50;
  font-size: 32px;
  margin-bottom: 40px;
}

.explanation-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}

.code-panel,
.explanation-panel {
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
  overflow: hidden;
}

.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  background: #f5f7fa;
  border-bottom: 1px solid #e4e7ed;
  font-weight: 600;
  color: #303133;
}

.btn-example,
.btn-copy {
  padding: 6px 16px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-example:hover,
.btn-copy:hover {
  background: #66b1ff;
}

.code-input {
  width: 100%;
  padding: 20px;
  border: none;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 14px;
  line-height: 1.6;
  resize: vertical;
  box-sizing: border-box;
}

.btn-explain {
  width: 100%;
  padding: 16px;
  background: #67c23a;
  color: white;
  border: none;
  border-top: 1px solid #e4e7ed;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-explain:hover:not(:disabled) {
  background: #85ce61;
}

.btn-explain:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.empty-state,
.loading-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 400px;
  padding: 40px;
  text-align: center;
}

.empty-icon {
  font-size: 64px;
  margin-bottom: 20px;
}

.empty-text {
  color: #909399;
  font-size: 15px;
}

.loader {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #409eff;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading-text {
  margin-top: 20px;
  color: #606266;
  font-size: 15px;
}

.explanation-content {
  padding: 24px;
  max-height: 800px;
  overflow-y: auto;
}

.section {
  margin-bottom: 30px;
}

.section:last-child {
  margin-bottom: 0;
}

.section-title {
  font-size: 18px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 16px;
  padding-bottom: 8px;
  border-bottom: 2px solid #409eff;
}

.section-content {
  color: #606266;
  font-size: 15px;
  line-height: 1.8;
}

.step-item {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
  padding: 16px;
  background: #f5f7fa;
  border-radius: 8px;
}

.step-number {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  line-height: 28px;
  text-align: center;
  background: #409eff;
  color: white;
  border-radius: 50%;
  font-weight: 600;
  font-size: 14px;
}

.step-content {
  flex: 1;
}

.step-code {
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 13px;
  color: #303133;
  background: white;
  padding: 8px 12px;
  border-radius: 4px;
  margin-bottom: 8px;
  border-left: 3px solid #409eff;
}

.step-desc {
  color: #606266;
  font-size: 14px;
  line-height: 1.6;
}

.concepts-list,
.notes-list,
.improvements-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.concept-item {
  padding: 14px;
  background: #ecf5ff;
  border-left: 3px solid #409eff;
  border-radius: 4px;
}

.concept-name {
  font-weight: 600;
  color: #409eff;
  margin-bottom: 6px;
  font-size: 14px;
}

.concept-desc {
  color: #606266;
  font-size: 14px;
  line-height: 1.6;
}

.note-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px;
  background: #fff7e6;
  border-left: 3px solid #ff9800;
  border-radius: 4px;
}

.note-icon {
  font-size: 18px;
  flex-shrink: 0;
}

.note-text {
  color: #606266;
  font-size: 14px;
  line-height: 1.6;
}

.improvement-item {
  padding: 16px;
  background: #f0f9ff;
  border-left: 3px solid #67c23a;
  border-radius: 4px;
}

.improvement-title {
  font-weight: 600;
  color: #67c23a;
  margin-bottom: 8px;
  font-size: 15px;
}

.improvement-desc {
  color: #606266;
  font-size: 14px;
  line-height: 1.6;
  margin-bottom: 10px;
}

.improvement-code {
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: #303133;
  background: white;
  padding: 12px;
  border-radius: 4px;
  margin: 0;
  white-space: pre-wrap;
  word-wrap: break-word;
}

@media (max-width: 1024px) {
  .explanation-layout {
    grid-template-columns: 1fr;
  }
}
</style>

四、单元测试生成实现

4.1 技术实现原理

核心思路

  1. 分析函数/类的输入输出
  2. 识别边界条件和异常情况
  3. 生成测试用例覆盖各种场景
  4. 使用主流测试框架(Jest/Vitest)
测试用例生成维度
  • 正常情况:常规输入输出
  • 边界条件:空值、极值、特殊字符
  • 异常情况:错误输入、异常处理
  • 性能测试:大数据量、并发

4.2 完整代码实现

vue
<template>
  <div class="test-generation-container">
    <h2>AI 单元测试生成</h2>

    <div class="generator-layout">
      <!-- 左侧:代码输入 -->
      <div class="code-section">
        <div class="section-header">
          <span>待测试代码</span>
          <select v-model="testFramework" class="framework-select">
            <option value="jest">Jest</option>
            <option value="vitest">Vitest</option>
            <option value="mocha">Mocha</option>
          </select>
        </div>
        <textarea
          v-model="sourceCode"
          placeholder="粘贴你的函数或类代码..."
          class="code-input"
          rows="15"
        ></textarea>
        <div class="options-panel">
          <label class="option-item">
            <input type="checkbox" v-model="options.includeEdgeCases" />
            <span>包含边界条件测试</span>
          </label>
          <label class="option-item">
            <input type="checkbox" v-model="options.includeErrorCases" />
            <span>包含异常情况测试</span>
          </label>
          <label class="option-item">
            <input type="checkbox" v-model="options.includeMocks" />
            <span>生成 Mock 示例</span>
          </label>
        </div>
        <button
          @click="generateTests"
          class="btn-generate"
          :disabled="generating || !sourceCode.trim()"
        >
          {{ generating ? '生成中...' : '生成测试用例' }}
        </button>
      </div>

      <!-- 右侧:生成的测试代码 -->
      <div class="test-section">
        <div class="section-header">
          <span>生成的测试代码</span>
          <button
            v-if="generatedTests"
            @click="copyTests"
            class="btn-copy"
          >
            复制代码
          </button>
        </div>

        <div v-if="!generatedTests && !generating" class="empty-state">
          <div class="empty-icon">🧪</div>
          <div class="empty-text">输入代码后点击"生成测试用例"</div>
        </div>

        <div v-if="generating" class="loading-state">
          <div class="loader"></div>
          <div class="loading-text">AI 正在分析代码并生成测试...</div>
        </div>

        <div v-if="generatedTests" class="test-result">
          <!-- 测试统计 -->
          <div class="test-stats">
            <div class="stat-item">
              <span class="stat-label">测试用例数:</span>
              <span class="stat-value">{{ generatedTests.testCount }}</span>
            </div>
            <div class="stat-item">
              <span class="stat-label">覆盖率预估:</span>
              <span class="stat-value">{{ generatedTests.coverage }}%</span>
            </div>
            <div class="stat-item">
              <span class="stat-label">框架:</span>
              <span class="stat-value">{{ testFramework }}</span>
            </div>
          </div>

          <!-- 测试代码 -->
          <pre class="test-code">{{ generatedTests.code }}</pre>

          <!-- 测试说明 -->
          <div v-if="generatedTests.explanation" class="test-explanation">
            <div class="explanation-title">测试说明</div>
            <div class="explanation-content">{{ generatedTests.explanation }}</div>
          </div>

          <!-- 运行指南 -->
          <div class="run-guide">
            <div class="guide-title">如何运行测试</div>
            <pre class="guide-commands">{{ generatedTests.runCommands }}</pre>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const sourceCode = ref(`function calculateTotal(items) {
  return items.reduce((sum, item) => {
    return sum + (item.price * item.quantity);
  }, 0);
}`)

const testFramework = ref('jest')
const generatedTests = ref(null)
const generating = ref(false)

const options = reactive({
  includeEdgeCases: true,
  includeErrorCases: true,
  includeMocks: false
})

// 生成测试用例
const generateTests = async () => {
  generating.value = true
  generatedTests.value = null

  try {
    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'claude-sonnet-4-20250514',
        max_tokens: 3000,
        messages: [{
          role: 'user',
          content: `请为以下代码生成单元测试:

\`\`\`javascript
${sourceCode.value}
\`\`\`

测试框架:${testFramework.value}
测试要求:
${options.includeEdgeCases ? '- 包含边界条件测试\n' : ''}${options.includeErrorCases ? '- 包含异常情况测试\n' : ''}${options.includeMocks ? '- 包含 Mock 示例\n' : ''}

请返回 JSON 格式:
{
  "code": "完整的测试代码",
  "testCount": 测试用例数量,
  "coverage": 预估覆盖率(0-100),
  "explanation": "测试用例说明(每个用例测试什么)",
  "runCommands": "运行测试的命令(多行字符串)"
}

要求:
1. 测试用例要全面,覆盖正常、边界、异常情况
2. 测试描述要清晰明确
3. 包含必要的 setup 和 teardown
4. 遵循测试框架的最佳实践

只返回JSON,不要其他内容。`
        }]
      })
    })

    const data = await response.json()
    const content = data.content[0].text
    const jsonMatch = content.match(/\{[\s\S]*\}/)

    if (jsonMatch) {
      generatedTests.value = JSON.parse(jsonMatch[0])
    }
  } catch (error) {
    console.error('测试生成失败:', error)
    alert('生成失败,请重试')
  } finally {
    generating.value = false
  }
}

// 复制测试代码
const copyTests = () => {
  navigator.clipboard.writeText(generatedTests.value.code)
  alert('测试代码已复制到剪贴板!')
}
</script>

<style scoped>
.test-generation-container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 30px;
}

h2 {
  text-align: center;
  color: #2c3e50;
  font-size: 32px;
  margin-bottom: 40px;
}

.generator-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}

.code-section,
.test-section {
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
  overflow: hidden;
}

.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  background: #f5f7fa;
  border-bottom: 1px solid #e4e7ed;
  font-weight: 600;
  color: #303133;
}

.framework-select {
  padding: 6px 12px;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  font-size: 13px;
  cursor: pointer;
}

.code-input {
  width: 100%;
  padding: 20px;
  border: none;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 14px;
  line-height: 1.6;
  resize: vertical;
  box-sizing: border-box;
}

.options-panel {
  padding: 16px 20px;
  background: #f5f7fa;
  border-top: 1px solid #e4e7ed;
  border-bottom: 1px solid #e4e7ed;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.option-item {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #606266;
  font-size: 14px;
  cursor: pointer;
}

.option-item input[type="checkbox"] {
  cursor: pointer;
}

.btn-generate {
  width: 100%;
  padding: 16px;
  background: #67c23a;
  color: white;
  border: none;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-generate:hover:not(:disabled) {
  background: #85ce61;
}

.btn-generate:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-copy {
  padding: 6px 16px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-copy:hover {
  background: #66b1ff;
}

.empty-state,
.loading-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 400px;
  padding: 40px;
  text-align: center;
}

.empty-icon {
  font-size: 64px;
  margin-bottom: 20px;
}

.empty-text {
  color: #909399;
  font-size: 15px;
}

.loader {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #67c23a;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading-text {
  margin-top: 20px;
  color: #606266;
  font-size: 15px;
}

.test-result {
  padding: 24px;
}

.test-stats {
  display: flex;
  gap: 30px;
  padding: 16px;
  background: #f0f9ff;
  border-radius: 8px;
  margin-bottom: 20px;
}

.stat-item {
  display: flex;
  align-items: center;
  gap: 6px;
}

.stat-label {
  color: #606266;
  font-size: 14px;
}

.stat-value {
  color: #409eff;
  font-weight: 600;
  font-size: 15px;
}

.test-code {
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: #303133;
  background: #1e1e1e;
  color: #d4d4d4;
  padding: 20px;
  border-radius: 8px;
  margin: 0 0 20px 0;
  white-space: pre-wrap;
  word-wrap: break-word;
  max-height: 500px;
  overflow-y: auto;
}

.test-explanation,
.run-guide {
  padding: 16px;
  border-radius: 8px;
  margin-bottom: 16px;
}

.test-explanation {
  background: #ecf5ff;
  border-left: 3px solid #409eff;
}

.run-guide {
  background: #e1f3d8;
  border-left: 3px solid #67c23a;
}

.explanation-title,
.guide-title {
  font-weight: 600;
  color: #303133;
  margin-bottom: 10px;
  font-size: 15px;
}

.explanation-content {
  color: #606266;
  font-size: 14px;
  line-height: 1.8;
  white-space: pre-wrap;
}

.guide-commands {
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: #303133;
  background: white;
  padding: 12px;
  border-radius: 4px;
  margin: 0;
  white-space: pre-wrap;
}

@media (max-width: 1024px) {
  .generator-layout {
    grid-template-columns: 1fr;
  }
}
</style>

五、简历撰写指南

5.1 项目经验描述模板

项目名称: AI 代码助手系统

项目时间: 2024.08 - 2024.12

项目描述: 开发基于 AI 的代码辅助工具,集成代码补全、代码解释和单元测试生成功能。通过 Claude API 理解代码上下文,为开发者提供智能编码建议,显著提升开发效率和代码质量。

核心职责

  1. 实现智能代码补全功能,基于上下文预测下一步代码,开发效率提升 40%
  2. 开发代码解释功能,将复杂代码转为通俗易懂的自然语言,降低代码理解成本 60%
  3. 实现单元测试自动生成,覆盖正常、边界、异常多种情况,测试覆盖率提升至 85%
  4. 优化 AI 调用策略,通过上下文提取和智能触发机制,降低 API 成本 50%

技术栈: Vue3 Composition API、Claude API、代码解析、AST 分析、防抖节流

项目成果
  • 团队 50+ 开发者使用,平均编码效率提升 35%
  • 减少 Code Review 时间 40%,代码质量明显提升
  • 新人上手时间从 2 周缩短至 3 天
  • 获得团队年度最佳工具奖

5.2 SOP 标准回答话术

面试官:介绍一下你做的 AI 代码助手项目

回答话术: "好的。这个项目是我开发的一个 AI 驱动的代码辅助工具,主要解决开发过程中的三个痛点:写代码慢、看代码难、测试用例难写。

先说背景。我们团队经常接手一些复杂的老项目,新人看代码很吃力,而且手写测试用例很费时间。另外开发时有些重复性的代码片段,手写效率低还容易出错。

针对这些问题,我做了三个核心功能:

第一个是智能代码补全。用户输入代码时,系统会分析上下文,预测接下来要写什么。实现上,我会提取光标前后各 10 行代码,加上文件顶部的导入语句和函数签名,一起发给 AI。AI 理解了上下文后生成补全建议。我设置了两种触发方式:一是用户输入特殊字符像 . ( { 这些,二是停顿 1 秒。建议会以浮层形式展示,用户按 Tab 接受,Esc 拒绝。这个功能让写代码效率提升了 40%。

第二个是代码解释。开发经常遇到看不懂的代码,我做了一个工具,用户粘贴代码,AI 会用自然语言解释。不是简单的翻译,而是从整体功能、执行流程、关键概念、注意事项等多个维度解释。还会指出潜在问题和改进建议。这对新人特别有用,理解代码的时间减少了 60%。

第三个是单元测试生成。手写测试用例很费时间,我让 AI 自动分析函数逻辑,生成测试用例。会覆盖正常情况、边界条件、异常处理等场景。还支持选择测试框架,生成的代码可以直接运行。实测团队的测试覆盖率从 50% 提升到了 85%。

技术难点主要是两个:一是上下文提取,要找到最相关的代码给 AI,既不能太少导致理解不准,也不能太多浪费 token。我的方案是提取光标前后各 10 行加关键定义。二是如何优化 AI 调用,我用了防抖策略和智能触发,把成本降低了 50%。

最终效果很好,现在团队 50 多人在用,平均编码效率提升 35%,新人上手时间从 2 周缩短到 3 天。"

面试官:代码补全的上下文提取为什么选择前后各 10 行?

回答话术: "这是个很好的问题。前后 10 行其实是我经过实验得出的一个平衡值。

最开始我提取的是前后 20 行,发现两个问题:

  1. Token 消耗太大,一次补全要用 500-800 token
  2. 上下文太多反而干扰 AI,它可能关注不到真正相关的部分

后来改成前后 5 行,发现上下文不够,AI 理解不准。比如用户在写一个函数内部的逻辑,但函数签名在 5 行之外,AI 就不知道参数类型和返回值,补全效果很差。

最后定在前后 10 行,再额外加上:

  1. 文件顶部的 import 语句:知道有哪些库可用
  2. 当前函数的完整定义:理解函数上下文
  3. 相关的类型定义:如果是 TypeScript

这样控制在 300-400 token,既能让 AI 充分理解,又不会浪费太多成本。

另外,如果遇到特殊情况,比如用户在文件开头,我会多提取后面的内容;如果在文件结尾,会多提取前面的。保证 AI 始终有足够的上下文。

这个优化让补全准确率从 65% 提升到了 88%,同时 API 成本降低了 40%。"

面试官:如何保证生成的单元测试是高质量的?

回答话术: "保证测试质量我主要从三个方面入手:

第一是测试用例的全面性。我在 Prompt 里明确要求 AI 生成三类测试:

  1. 正常情况:常规输入输出
  2. 边界条件:空值、null、undefined、极值、空数组等
  3. 异常情况:错误参数、类型不匹配、异常抛出

比如测试一个数组求和函数,不只是测 [1,2,3],还会测空数组、包含负数、包含非数字等情况。

第二是测试描述的清晰性。每个测试用例的描述要明确说明"在什么情况下,应该得到什么结果"。这样后续维护时能快速理解测试意图。我会在 Prompt 里强调这一点。

第三是可运行性。生成的代码要能直接跑起来,不需要手动修改。我会:

  1. 指定测试框架和版本
  2. 包含必要的 import
  3. 如果需要 Mock,也生成相应代码
  4. 提供运行命令

另外,我还做了一个验证机制:生成测试后,会让 AI 自我审查,看有没有遗漏的场景。如果覆盖率预估低于 80%,会提示用户手动补充。

实测效果:生成的测试用例可用率达 92%,团队的测试覆盖率从 50% 提升到 85%,而且 Code Review 发现的 bug 减少了 40%。"

5.3 难点与亮点分析

难点1:上下文窗口的动态调整

问题描述: 不同场景需要的上下文量不同,固定提取可能不够准确或浪费 token。

解决方案
  1. 基础策略:光标前后各 10 行
  2. 智能扩展:
    • 如果在函数内,自动包含函数签名
    • 如果有 import,包含相关导入
    • 如果是类方法,包含类定义
  3. 动态压缩:如果上下文超长,优先保留:
    • 最近的代码(离光标近)
    • 函数/类定义
    • 导入语句
技术实现
javascript
const extractSmartContext = (code, cursorPos) => {
  const lines = code.split('\n')
  const currentLineNum = code.substring(0, cursorPos).split('\n').length

  let context = {
    before: lines.slice(Math.max(0, currentLineNum - 10), currentLineNum),
    after: lines.slice(currentLineNum, currentLineNum + 10)
  }

  // 查找并包含函数定义
  const funcDef = findEnclosingFunction(lines, currentLineNum)
  if (funcDef) context.funcDef = funcDef

  // 包含 import 语句
  const imports = lines.filter(line => line.trim().startsWith('import'))
  context.imports = imports

  return context
}

难点2:补全建议的触发时机

问题描述: 过于频繁触发会打扰用户,太少又错过补全机会,需要找到平衡点。

解决方案
  1. 多种触发方式:
    • 特殊字符:. ( { [ 等立即触发
    • 停顿触发:用户停止输入 1 秒后触发
    • 手动触发:用户按快捷键(Ctrl+Space)
  2. 智能过滤:
    • 输入少于 2 个字符不触发
    • 在字符串/注释内不触发
    • 连续触发间隔至少 500ms
  3. 用户行为学习:
    • 记录用户接受/拒绝补全的频率
    • 动态调整触发策略

难点3:测试用例的覆盖度保证

问题描述: 如何确保生成的测试用例能覆盖各种场景,不遗漏关键情况。

解决方案
  1. 结构化 Prompt:明确列出必须包含的测试类型
  2. 代码分析:
    • 识别函数的参数类型
    • 提取返回值类型
    • 检测异常处理逻辑
  3. 二次验证:
    • AI 生成后自我审查
    • 计算预估覆盖率
    • 提示遗漏的场景

亮点1:渐进式补全策略

不是一次性生成大段代码,而是根据用户输入逐步补全:

  • 输入函数名 → 补全函数签名
  • 输入参数 → 补全函数体框架
  • 输入具体逻辑 → 补全细节代码

这种渐进式方式让用户保持对代码的掌控感。

亮点2:代码解释的多维度分析

不只是简单翻译,而是从多个角度解释:

  • 整体功能:这段代码做什么
  • 执行流程:代码如何运行
  • 关键概念:用到的技术点
  • 注意事项:潜在的问题
  • 改进建议:如何优化

亮点3:测试生成的框架适配

支持多种测试框架,生成的代码符合各框架的最佳实践:

  • Jest:使用 describe/it/expect
  • Vitest:支持 Vite 生态
  • Mocha:配合 Chai 断言库

5.4 避免 AI 化的表达技巧

AI 化表达: "利用先进的大语言模型和深度学习算法,构建了一个智能化的代码辅助系统,实现了代码补全、解释和测试生成的全面自动化。"

自然表达: "开发了一个 AI 代码助手,能根据你正在写的代码预测下一步,还能把复杂代码解释成通俗的话,自动生成测试用例。就像有个老司机在旁边指导你写代码。"

关键差异

  1. 用比喻而非技术术语
  2. 强调用户体验而非技术实现
  3. 用具体场景而非抽象功能
  4. 口语化而非书面语

5.5 完整简历示例

plain
【AI 代码助手系统】2024.08 - 2024.12

项目背景:
团队开发效率有提升空间,新人看代码困难,手写测试用例费时,需要智能工具辅助开发。

我的工作:
1. 智能代码补全功能开发
   - 实现基于上下文的代码预测,提取光标前后 10 行 + 函数定义 + 导入语句
   - 设计双重触发机制:特殊字符立即触发 + 停顿 1 秒触发
   - 通过防抖和智能缓存,将 API 调用成本降低 50%
   - 编码效率提升 40%,补全准确率达 88%

2. 代码解释功能实现
   - 开发多维度代码分析:整体功能、执行流程、关键概念、改进建议
   - 实现自然语言转换,将技术代码转为通俗易懂的描述
   - 代码理解时间减少 60%,新人上手周期从 2 周缩短至 3 天

3. 单元测试自动生成
   - 实现测试用例智能生成,覆盖正常、边界、异常多种情况
   - 支持 Jest/Vitest/Mocha 三种框架,生成代码可直接运行
   - 测试覆盖率从 50% 提升至 85%,可用率达 92%
   - Code Review 发现的 bug 减少 40%

4. 性能优化与用户体验
   - 优化上下文提取算法,平衡准确性和成本,token 使用量降低 40%
   - 实现补全建议的智能过滤,避免过度打扰用户
   - 设计渐进式补全策略,保持用户对代码的掌控感

技术栈:Vue3、Claude API、AST 解析、防抖节流、代码分析

项目成果:
- 团队 50+ 开发者使用,平均编码效率提升 35%
- 新人培训周期缩短 77%(从 2 周到 3 天)
- Code Review 时间减少 40%,代码质量明显提升
- 获得团队年度最佳工具奖,被推广至其他部门

六、运行说明

6.1 环境准备

bash
npm create vite@latest code-assistant -- --template vue
cd code-assistant
npm install

6.2 运行代码

  1. 将三个功能的代码分别复制到 src/App.vue
  2. 运行 npm run dev
  3. 浏览器访问 http://localhost:5173

6.3 功能演示

  • 代码补全:在编辑器输入代码,停顿或输入特殊字符触发补全
  • 代码解释:粘贴复杂代码,查看 AI 的多维度解释
  • 测试生成:输入函数代码,自动生成全面的测试用例

核心要点

  • 上下文提取要精准,平衡准确性和成本
  • 触发时机要智能,避免过度打扰用户
  • 测试用例要全面,覆盖各种边界情况
  • 简历表达要具体、自然、有数据支撑
  • 面试回答要有深度思考和解决方案