从零部署国产高精度OCR:DeepSeek-OCR-WEBUI集成实践指南

从零部署国产高精度OCR:DeepSeek-OCR-WEBUI集成实践指南

在数字化办公和智能文档处理日益普及的今天,高效、精准的OCR(光学字符识别)能力已成为企业自动化流程中的关键一环。尤其对于中文场景下的复杂文本识别——如票据、表格、手写体等——传统OCR工具往往力不从心。

而近期由DeepSeek开源推出的DeepSeek-OCR-WEBUI镜像,为这一难题提供了极具竞争力的国产化解决方案。它不仅具备强大的多语言识别能力,更在中文长文本、结构化内容(如表格与公式)识别上表现出色,且支持一键部署、Web交互与OpenAI协议兼容调用。

本文将带你从零开始完整部署 DeepSeek-OCR-WEBUI,涵盖环境准备、服务启动、前端使用及API集成全过程,确保即使你是AI新手也能快速上手,真正实现“本地运行、即插即用”的高精度OCR体验。


1. 为什么选择 DeepSeek-OCR?

在介绍如何部署之前,先来看看这款模型究竟解决了哪些痛点:

  • 中文识别精度高:针对中文排版、字体、语义优化,远超通用OCR引擎。
  • 复杂场景鲁棒性强:倾斜、模糊、低分辨率图像仍能准确提取文字。
  • 结构还原能力强:可保留标题层级、列表、代码块、数学公式等格式信息。
  • 轻量化部署:单张显卡(如4090D)即可运行,适合本地或边缘设备。
  • 双模式访问:既可通过网页直接操作,也支持程序化API调用。
  • 完全开源可控:无数据外泄风险,适用于金融、政务等敏感领域。
简而言之:如果你需要一个中文强、速度快、格式保真度高、本地可运行的OCR系统,DeepSeek-OCR 是目前最值得尝试的选择之一。

2. 部署前准备:软硬件要求与依赖安装

2.1 硬件建议

组件推荐配置
GPUNVIDIA RTX 4090D 或同等性能及以上显卡(显存 ≥ 24GB)
CPU多核处理器(Intel i7 / AMD Ryzen 7 及以上)
内存≥ 32GB
存储≥ 50GB 可用空间(含模型缓存)
注:若仅用于测试,也可在CPU模式下运行,但推理速度会显著下降。

2.2 软件环境

  • 操作系统:Linux(Ubuntu 20.04+)或 Windows WSL2
  • Python版本:3.12+
  • 包管理工具:推荐使用 condavenv

2.3 创建独立虚拟环境并安装依赖

# 创建虚拟环境 conda create -n deepseekocr python=3.12.9 conda activate deepseekocr # 安装核心依赖 pip install torch==2.6.0 transformers==4.46.3 tokenizers==0.20.3 \ einops addict easydict python-multipart uvicorn fastapi \ Pillow torchvision requests 
提示:若你的GPU支持Flash Attention,可额外安装 flash-attn 以提升推理效率并降低显存占用。

3. 项目结构搭建:组织代码与静态资源

我们采用简洁清晰的目录结构来管理整个OCR服务:

deepseek-ocr-project/ ├── app.py # FastAPI后端主程序 ├── static/ │ └── ui.html # 前端网页界面 └── README.md # 说明文档(可选) 

你可以通过以下命令快速创建该结构:

mkdir -p deepseek-ocr-project/static cd deepseek-ocr-project touch app.py 

接下来我们将分别填充 app.pyui.html 文件内容。


4. 后端服务搭建:基于 FastAPI 的 OpenAI 兼容接口

我们将使用 FastAPI 构建一个兼容 OpenAI 协议的OCR服务,使其不仅能被网页调用,还能无缝接入现有AI工作流。

4.1 编写 app.py 主程序

将以下完整代码保存至 app.py

import os import time import uuid import base64 import tempfile import mimetypes import logging from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse import requests import torch from fastapi import FastAPI, File, UploadFile, Form, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from transformers import AutoModel, AutoTokenizer # ---------------- logging ---------------- logging.basicConfig(level=logging.INFO) log = logging.getLogger("ocr-api") # ---------------- app & CORS ------------- app = FastAPI(title="Transformers模型服务 (OpenAI-Compatible)") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 静态目录(用于放置你的 ui.html) STATIC_DIR = os.getenv("STATIC_DIR", "static") os.makedirs(STATIC_DIR, exist_ok=True) app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") # 便捷入口:/ui -> /static/ui.html(可选) @app.get("/ui") async def ui_redirect(): html = '<meta http-equiv="refresh" content="0; url=/static/ui.html" />' return HTMLResponse(content=html, status_code=200) # ---------------- model load ------------- os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0") MODEL_NAME = os.getenv("DEEPSEEK_OCR_PATH", "deepseek-ai/DeepSeek-OCR") # 支持本地路径或HuggingFace ID OPENAI_MODEL_ID = "deepseek-ocr" tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) model = AutoModel.from_pretrained( MODEL_NAME, trust_remote_code=True, use_safetensors=True, ) # 设备与精度设置 if torch.cuda.is_available(): device = torch.device("cuda:0") model = model.eval().to(device) try: model = model.to(torch.bfloat16) except Exception: try: model = model.to(torch.float16) log.info("BF16 不可用,已回退到 FP16") except Exception: model = model.to(torch.float32) log.info("FP16 不可用,已回退到 FP32") else: device = torch.device("cpu") model = model.eval().to(device) log.warning("未检测到 CUDA,将在 CPU 上推理。") # ---------------- helpers ---------------- def _now_ts() -> int: return int(time.time()) def _gen_id(prefix: str) -> str: return f"{prefix}_{uuid.uuid4().hex[:24]}" def _save_bytes_to_temp(data: bytes, suffix:) -> str: tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) tmp.write(data) tmp.flush() tmp.close() return tmp.name def _is_data_uri(url: str) -> bool: return isinstance(url, str) and url.startswith("data:") def _is_local_like(s: str) -> bool: if not isinstance(s, str): return False if s.startswith("file://"): return True parsed = urlparse(s) if parsed.scheme in ("http", "https", "data"): return False return True def _to_local_path(s: str) -> str: if s.startswith("file://"): return s[7:] return os.path.expanduser(s) def _download_to_temp(url: str) -> str: if not isinstance(url, str) or not url.strip(): raise HTTPException(status_code=400, detail="Empty image url") # 1) data: URI if _is_data_uri(url): try: header, b64 = url.split(",", 1) ext = ".bin" if "image/png" in header: ext = ".png" elif "image/jpeg" in header or "image/jpg" in header: ext = ".jpg" elif "image/webp" in header: ext = ".webp" raw = base64.b64decode(b64) path = _save_bytes_to_temp(raw, suffix=ext) log.info(f"[image] data-uri -> {path}") return path except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid data URI: {e}") # 2) 本地文件 if _is_local_like(url): p = _to_local_path(url) if not os.path.isabs(p): p = os.path.abspath(p) if not os.path.isfile(p): raise HTTPException(status_code=400, detail=f"Local file not found or not a file: {p}") ext = os.path.splitext(p)[1] or ".img" try: with open(p, "rb") as f: data = f.read() except Exception as e: raise HTTPException(status_code=400, detail=f"Read local file failed: {p} ({e})") path = _save_bytes_to_temp(data, suffix=ext) log.info(f"[image] local -> {p} -> {path}") return path # 3) http(s) try: log.info(f"[image] http(s) -> {url}") resp = requests.get(url, timeout=30) resp.raise_for_status() ctype = resp.headers.get("Content-Type", "") ext = mimetypes.guess_extension(ctype) or ".img" path = _save_bytes_to_temp(resp.content, suffix=ext) log.info(f"[image] http(s) saved -> {path}") return path except Exception as e: raise HTTPException(status_code=400, detail=f"Download image failed: {e}") def _extract_text_and_first_image_from_messages(messages: List[Dict[str, Any]]) -> Tuple[str, Optional[str]]: all_text: List[str] = [] image_path: Optional[str] = None for msg in messages: content = msg.get("content") if content is None: continue if isinstance(content, str): all_text.append(content) continue if isinstance(content, list): for part in content: ptype = part.get("type") if ptype in ("text", "input_text"): txt = part.get("text", "") if isinstance(txt, str) and txt.strip(): all_text.append(txt) elif ptype in ("image_url", "input_image"): if image_path is None: image_field = part.get("image_url") or part.get("image") url = image_field.get("url") if isinstance(image_field, dict) else image_field if not url or not isinstance(url, str): raise HTTPException(status_code=400, detail="image_url is missing or invalid") image_path = _download_to_temp(url) prompt = "\n".join([t for t in all_text if t.strip()]) if all_text else "" return prompt, image_path def _run_ocr_infer(prompt: str, image_path: str) -> str: full_prompt = f"<image>\n{prompt}".strip() try: res = model.infer( tokenizer, prompt=full_prompt, image_file=image_path, output_path="./save", base_size=1024, image_size=640, crop_mode=True, save_results=False, test_compress=True, eval_mode=True, ) except Exception as e: raise HTTPException(status_code=500, detail=f"Infer failed: {e}") if isinstance(res, dict): for key in ("text", "result", "output", "ocr_text"): if key in res and isinstance(res[key], str): return res[key] return str(res) if isinstance(res, (list, tuple)): return "\n".join(map(str, res)) return str(res) def _token_count_approx(text: str) -> int: try: return len(tokenizer.encode(text)) except Exception: return max(1, len(text) // 4) # ---------------- routes ---------------- @app.get("/health") async def health_check(): return {"status": "healthy"} @app.post("/parserToText") async def parser_to_text(file: UploadFile = File(...), content: str = Form(...)): file_bytes = await file.read() suffix = os.path.splitext(file.filename or "")[1] or ".img" tmp_path = _save_bytes_to_temp(file_bytes, suffix=suffix) prompt = "<image>\n" + (content or "") try: res = model.infer( tokenizer, prompt=prompt, image_file=tmp_path, output_path="./save", base_size=1024, image_size=640, crop_mode=True, save_results=False, test_compress=True, eval_mode=True, ) return res except Exception as e: return {"status": "error", "message": str(e)} finally: if os.path.exists(tmp_path): try: os.unlink(tmp_path) except Exception: pass @app.get("/v1/models") async def list_models(): return { "object": "list", "data": [{"id": OPENAI_MODEL_ID, "object": "model", "created": _now_ts(), "owned_by": "owner"}], } @app.post("/v1/chat/completions") async def chat_completions(request: Request): payload = await request.json() messages = payload.get("messages") if not isinstance(messages, list) or not messages: raise HTTPException(status_code=400, detail="`messages` must be a non-empty list") prompt_text, image_path = _extract_text_and_first_image_from_messages(messages) if not image_path: raise HTTPException(status_code=400, detail="No image found in messages. Provide content with type='image_url'.") try: answer = _run_ocr_infer(prompt_text, image_path) finally: if image_path and os.path.exists(image_path): try: os.unlink(image_path) except Exception: pass prompt_tokens = _token_count_approx(prompt_text) completion_tokens = _token_count_approx(answer) return JSONResponse({ "id": _gen_id("chatcmpl"), "object": "chat.completion", "created": _now_ts(), "model": OPENAI_MODEL_ID, "choices": [{"index": 0, "message": {"role": "assistant", "content": answer}, "finish_reason": "stop"}], "usage": {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": prompt_tokens + completion_tokens}, }) # 根路由 @app.get("/") async def root(): return {"service": "OpenAI-Compatible OCR Service", "model": OPENAI_MODEL_ID, "ui": "/static/ui.html"} # ---------------- main ---------------- if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001) 

5. 前端页面开发:构建简洁易用的 Web UI

5.1 创建 static/ui.html

static/ 目录下新建 ui.html 文件,并粘贴以下HTML代码:

<!doctype html> <html lang="zh"> <head> <meta charset="utf-8"> <title>DeepSeek-OCR • Web UI</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> :root { --bg:#0b1220; --fg:#e6edf3; --muted:#9aa4b2; --acc:#49b5ff; --card:#111a2e; --ok:#2ecc71; --err:#ff6b6b; } * { box-sizing: border-box; } body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial; background:var(--bg); color:var(--fg); } .wrap { max-width: 1000px; margin: 32px auto; padding: 0 16px; } h1 { font-weight:700; margin: 0 0 6px; } p.desc { color:var(--muted); margin: 0 0 16px; } .card { background:var(--card); border-radius:16px; padding:16px; box-shadow: 0 10px 30px rgba(0,0,0,.25); margin-bottom:16px; } .row { display:flex; gap:16px; flex-wrap:wrap; } .col { flex:1 1 360px; min-width:320px; } label { font-size:14px; color:var(--muted); display:block; margin:6px 0; } input[type="text"], textarea, select { width:100%; background:#0e1627; color:var(--fg); border:1px solid #1e2b44; border-radius:12px; padding:10px 12px; outline:none; font-size:14px; } textarea { min-height:120px; resize:vertical; } .btn { background:var(--acc); color:#001224; border:none; border-radius:12px; padding:10px 16px; font-weight:700; cursor:pointer; } .btn:disabled { opacity:.6; cursor:not-allowed; } .pill { display:inline-block; background:#0e1627; border:1px dashed #1e2b44; color:var(--muted); border-radius:999px; padding:6px 10px; font-size:12px; } #preview { max-width:100%; max-height:260px; border-radius:12px; border:1px solid #1e2b44; display:none; margin-top:8px; } .out { white-space:pre-wrap; background:#0e1627; border:1px solid #1e2b44; border-radius:12px; padding:12px; min-height:140px; } .tabs { display:flex; gap:8px; margin-top:8px; } .tabs button { background:#0e1627; color:var(--muted); border:1px solid #1e2b44; border-radius:10px; padding:6px 10px; cursor:pointer; } .tabs button.active { color:var(--fg); border-color:var(--acc); } a { color: var(--acc); text-decoration: none; } .row-compact { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } .muted { color:var(--muted); font-size:12px; } </style> </head> <body> <div> <h1>DeepSeek-OCR Web UI</h1> <p>上传图片 + 输入提示,直接调用后端 <code>/v1/chat/completions</code>。默认预设:<span>返回 Markdown 识别结果</span></p> <div> <div> <div> <label>图片文件</label> <input type="file" accept="image/*"> <img alt="preview"> <div>前端会把图片转为 <code>data:</code> Base64 发送到后端。</div> </div> <div> <label>预设指令</label> <select> <option value="md" selected>返回 Markdown 识别结果(保留标题/列表/表格/代码块)</option> <option value="plain">返回纯文本(仅文字内容,去版式)</option> <option value="json">返回 JSON 结构:{title, paragraphs, tables[], figures[]}</option> </select> <label>自定义提示(可选,会拼接到预设后面)</label> <textarea placeholder="例如:表格务必用标准 Markdown 表格语法;公式用 $...$;图片题注前缀用 Figure:"></textarea> <div> <button>识别并生成</button> <span>就绪</span> </div> <div> 接口地址:<code>/v1/chat/completions</code>(同源部署可直接使用) </div> </div> </div> </div> <div> <div> <button>原始文本</button> <button>Markdown 预览</button> </div> <div></div> <div></div> </div> <div>API: <a href="/v1/models" target="_blank">/v1/models</a> · <a href="/health" target="_blank">/health</a></div> </div> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script> const fileEl = document.getElementById('file'); const preview = document.getElementById('preview'); const presetEl = document.getElementById('preset'); const promptEl = document.getElementById('prompt'); const runBtn = document.getElementById('run'); const statusEl = document.getElementById('status'); const rawEl = document.getElementById('raw'); const mdEl = document.getElementById('md'); const tabRaw = document.getElementById('tab-raw'); const tabMd = document.getElementById('tab-md'); function endpoint() { return '/v1/chat/completions'; } function presetText(key) { if (key === 'plain') { return "请输出纯文本的 OCR 结果,仅保留文字内容,去掉所有版式与装饰符号。"; } else if (key === 'json') { return "请以 JSON 返回 OCR 结果,字段为 {title, paragraphs, tables: [markdown_table], figures: [caption]},不要解释说明。"; } return "请以 Markdown 返回 OCR 结果,尽量还原版式:使用 # 标题、无序/有序列表、```代码块、表格用标准 Markdown 表格语法;无法识别的片段用 [UNCERTAIN] 标记。"; } function fileToDataURI(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error('读取文件失败')); reader.onload = () => resolve(reader.result); reader.readAsDataURL(file); }); } function setTab(which) { if (which === 'raw') { tabRaw.classList.add('active'); tabMd.classList.remove('active'); rawEl.style.display = 'block'; mdEl.style.display = 'none'; } else { tabMd.classList.add('active'); tabRaw.classList.remove('active'); mdEl.style.display = 'block'; rawEl.style.display = 'none'; } } function setStatus(text, ok=true) { statusEl.textContent = text; statusEl.style.borderColor = ok ? '#1e2b44' : 'var(--err)'; statusEl.style.color = ok ? 'var(--muted)' : '#ffdede'; } fileEl.addEventListener('change', () => { const f = fileEl.files && fileEl.files[0]; if (!f) { preview.style.display = 'none'; return; } const url = URL.createObjectURL(f); preview.src = url; preview.style.display = 'block'; }); tabRaw.onclick = () => setTab('raw'); tabMd.onclick = () => setTab('md'); runBtn.addEventListener('click', async () => { try { const f = fileEl.files && fileEl.files[0]; if (!f) { alert('请先选择图片文件'); return; } const dataUri = await fileToDataURI(f); const preset = presetText(presetEl.value); const custom = (promptEl.value || '').trim(); const textMsg = custom ? (preset + "\n\n" + custom) : preset; const body = { model: "deepseek-ocr", messages: [ { role: "user", content: [ { type: "text", text: textMsg }, { type: "image_url", image_url: { url: dataUri } } ] } ], }; setStatus('识别中…', true); runBtn.disabled = true; rawEl.textContent = ''; mdEl.textContent = ''; const t0 = performance.now(); const resp = await fetch(endpoint(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const t1 = performance.now(); if (!resp.ok) { const errText = await resp.text(); setStatus('出错', false); rawEl.textContent = `HTTP ${resp.status}\n${errText}`; setTab('raw'); return; } const json = await resp.json(); const content = json?.choices?.[0]?.message?.content ?? ''; rawEl.textContent = content || '[空响应]'; if (window.marked && content) { mdEl.innerHTML = marked.parse(content); } else { mdEl.textContent = content; } setStatus(`完成(${((t1 - t0)/1000).toFixed(2)}s)`, true); } catch (e) { setStatus('出错', false); rawEl.textContent = String(e?.stack || e); setTab('raw'); } finally { runBtn.disabled = false; } }); </script> </body> </html> 

6. 启动服务并访问 WebUI

6.1 启动后端服务

确保你已激活虚拟环境并位于项目根目录:

python app.py 

服务默认监听 http://0.0.0.0:8001,启动成功后你会看到类似日志:

INFO: Started server process [PID] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8001 

6.2 访问 Web 界面

打开浏览器,访问:

http://localhost:8001/ui 

你将看到如下界面:

  • 左侧上传图片区域
  • 右侧选择预设指令(Markdown / 纯文本 / JSON)
  • 底部展示识别结果,支持原始文本与Markdown预览切换

点击【识别并生成】按钮,即可获得高质量OCR输出。


7. API 调用示例:Python客户端集成

除了网页使用,你还可以通过标准OpenAI SDK方式调用该服务。

7.1 安装 OpenAI Python 包

pip install openai 

7.2 调用代码示例

from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8001/v1", api_key="sk-x") response = client.chat.completions.create( model="deepseek-ocr", messages=[ { "role": "user", "content": [ {"type": "text", "text": "请以Markdown格式输出OCR结果,保留表格与代码块"}, {"type": "image_url", "image_url": {"url": "file:///path/to/your/document.png"}} ] } ] ) print(response.choices[0].message.content) 
支持输入类型包括:data: Base64、本地路径、HTTP链接。

8. 使用技巧与常见问题

8.1 提升识别质量的小技巧

  • 优先使用高清图片:分辨率越高,识别越准。
  • 避免过度压缩JPEG:可能导致文字边缘失真。
  • 添加明确提示词:如“请识别为Markdown格式”、“保留原始段落结构”。
  • 对表格图片强调:“请用标准Markdown表格语法输出”。

8.2 常见问题排查

问题解决方案
启动时报错 ModuleNotFoundError检查是否安装了所有依赖包
图片上传后无响应查看控制台日志,确认临时文件路径权限
中文识别乱码或缺失确保使用的是官方 deepseek-ai/DeepSeek-OCR 模型
显存不足尝试关闭 flash_attention 或更换为FP32精度

9. 总结

通过本文的详细指导,你应该已经成功部署并运行了 DeepSeek-OCR-WEBUI,实现了以下目标:

  • 成功搭建本地OCR服务
  • 实现网页端可视化操作
  • 支持OpenAI协议API调用
  • 掌握实际使用技巧与调试方法

DeepSeek-OCR 不仅是当前国产OCR技术的佼佼者,更是企业级文档自动化、教育数字化、档案电子化的理想选择。其出色的中文识别能力和灵活的部署方式,让它成为替代商业OCR服务的有力竞争者。

下一步,你可以尝试将其集成进PDF批处理流水线、合同审核系统或知识库构建平台,真正发挥其价值。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 ZEEKLOG星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Read more

对于VScode中Copilot插件使用卡顿问题的解决办法

copilot卡顿主要是网络和内存占用原因。 VScode内存优化解决办法: 结合链接和我补充的基本都可以解决。 解决VSCode无缘无故卡顿的问题_vscode卡顿-ZEEKLOG博客 在VScode中打开setting.json文件,打开方法ctrl+shift+p,输入Preferences: Open User Settings (JSON), 然后添加如下代码: { "search.followSymlinks": false, "git.autorefresh": false, "editor.formatOnSave": false } 结合链接和我补充的基本都可以解决。 VScode代理问题: vscode copilot长时间没反应_vscode中copilot总是卡住-ZEEKLOG博客 配置代理的话两种方法,上面是一种,推荐两种结合起来用(不冲突) 还是在setting.json文件中,添加如下代码: { "http.proxy": "http://127.

无需任何拓展Copilot接入第三方OpenAI接口教程

禁止搬运,转载需标明本文链接 省流:修改"C:\Users\你的用户名称\.vscode\extensions\github.copilot-chat-0.35.0\package.json"中的"when": "productQualityType != 'stable'"为"when": "productQualityType == 'stable'",即可在copilot添加支持openAI的第三方接口 我在寻找怎么让copilot接入第三方接口的时候,通过别人的贴子(长期有效)接入第三方 OpenAI 兼容模型到 GitHub Copilot-ZEEKLOG博客发现了官方的讨论Add custom OpenAI endpoint configuration

新手如何用AI写小说?全流程教学+3款好用的AI写作软件推荐(附提示词)

新手如何用AI写小说?全流程教学+3款好用的AI写作软件推荐(附提示词)

最近后台私信都要爆了,好多粉丝朋友问我:“大大,我也想做自媒体写网文,但每次对着空白文档就想把键盘吃了怎么办?”、“大纲写得跟流水账一样,还没写到第十章就崩了……” 作为一个在码字圈摸爬滚打多年,掉过无数坑、也熬过无数通宵的写小说的老兵,我太懂这种“甚至不知道第一句话该写什么”的崩溃感了。 这两年为了找顺手的工具,我当真是神农尝百草,把市面上主流的ai写小说工具试了个遍。接下来我会教大家怎么用通用大模型进行一些写作前的准备(包括提示词),准备工作充足后再开始用AI写作工具填充内容。 一、写作前的准备工作 1、确定小说类型与主题 在开始创作前,先明确小说的基本方向: * 赛道选择: 明确核心流派(如赛博朋克、修真、本格推理等)与细分领域。 * 内核设定: 提炼作品的中心思想与核心价值观(Theme)。 * 受众锚定: 清晰画像目标读者群体。 * 竞品调研: 分析市场风向,寻找题材蓝海。 提示词: 请扮演一位资深网文编辑,分析当前玄幻小说市场的热门趋势。请提供 5个具有爆款潜力的创新主题。对于每个主题,请包含以下内容: 核心概念: 一句话概括故事内核。 世界观

彻底解决 Codex / Copilot 修改中文乱码【含自动化解决方案】

彻底解决 Codex / Copilot 修改中文乱码【含自动化解决方案】

引言 在使用 GitHub Copilot 或 OpenAI Codex 自动重构代码时,你是否遇到过这样的尴尬:AI 生成的代码逻辑完美,但原本注释里的中文却变成了 我爱中文 这样的乱码?有时候这种字符甚至会污染正确的代码,带来巨大的稳定性隐患。 一、 问题核心:被忽视的“终端中转” 乱码的根源不在于 AI 的大脑,也不在于编辑器的显示,而在于执行链路的编码不一致。 Copilot/Codex 在执行某些修改任务(如:重构整个文件或批量替换)时,往往会通过终端调用系统指令。由于 Windows 终端(PowerShell/CMD)默认使用 GBK 编码,它在处理 AI 传来的 UTF-8 字节时会发生“误读”,导致写入文件的内容从源头上就损坏了。