本地 LLM 翻译实战

举报
yd_244592142 发表于 2026/11/08 10:27:36 2026/11/08
【摘要】 一、为什么用本地 LLM 做翻译先说清楚场景。不是 Google 翻译那种"输入一个词吐一个词"的轻量需求,而是:隐私敏感文本:公司内部文档、医疗病历、法律合同——不能发到第三方 API。批量长文本:几万字的技术文档、小说章节,需要一次性处理且可控成本。领域定制:通用翻译把"梯度下降"翻成"gradient descent"(没翻),但你想要的是带术语表的、风格统一的结果。离线环境:内网部...

一、为什么用本地 LLM 做翻译

先说清楚场景。不是 Google 翻译那种"输入一个词吐一个词"的轻量需求,而是:

  • 隐私敏感文本:公司内部文档、医疗病历、法律合同——不能发到第三方 API。
  • 批量长文本:几万字的技术文档、小说章节,需要一次性处理且可控成本。
  • 领域定制:通用翻译把"梯度下降"翻成"gradient descent"(没翻),但你想要的是带术语表的、风格统一的结果。
  • 离线环境:内网部署,压根没有外网出口。

本地 LLM 的优势就在这里:数据不出机器、调用不限次、提示词可以极度定制。但代价是——你得自己把提示词和调用链路调好,没有 OpenAI 那边帮你做好的各种"对齐"兜底。


二、提示词设计:翻译的核心不是"翻译",是"约束"

很多人写翻译提示词就一句话:

请把以下内容翻译成英文:{text}

能用吗?能。好用吗?不好用。常见问题:

  1. 术语不一致:同一个词上下文翻成两种说法。
  2. 风格漂移:技术文档翻着翻着变成口语化。
  3. 漏译 / 加译:模型自作主张删掉"不重要的"部分,或者加自己的解释。
  4. 格式丢失:Markdown / 代码块 / 表格被破坏。

根因是:翻译任务对"忠实度"的要求远高于对话任务,但默认的提示词没有任何约束机制。

2.1 一个经过实战检验的提示词模板

你是一位专业的{source_lang}→{target_lang}翻译专家,擅长{domain}领域。

## 翻译规则
1. 严格逐句翻译,不遗漏、不新增、不合并句子。
2. 保持原文的段落结构和换行。
3. 保持原文中的 Markdown 格式标记(标题、列表、代码块、加粗等)不变。
4. 代码块内容不翻译,只翻译代码注释。
5. 专有名词和术语按以下术语表翻译:
   {glossary}
6. 语气保持{tone}风格。

## 术语表
{glossary}

## 原文
{text}

## 译文(直接输出翻译结果,不要加任何解释或前后缀)

几个关键设计点:

  • “逐句翻译,不遗漏不新增”:这是最重要的约束。没有这句话,模型有概率"意译"甚至跳过长段落。
  • 格式保持指令:明确点名 Markdown 标记,否则模型会把 ## 标题 翻成 **标题** 之类。
  • 代码块不翻译:技术文档翻译的命门。不加这句,模型会把你代码里的字符串也给翻了。
  • 术语表:这是领域定制的核心,后面代码里会讲怎么动态注入。
  • “直接输出”:防止模型输出 “好的,以下是翻译结果:\n\n…” 这种废话前缀。

2.2 Few-shot:给两个例子比说一百句规则管用

当规则约束不住时,上例子。在提示词里塞 1-2 个翻译样本:

## 翻译示例

示例1:
原文:该模型在ImageNet上达到了76.5%的top-1准确率。
译文:The model achieved 76.5% top-1 accuracy on ImageNet.

示例2:
原文:```python
# 初始化模型
model = ResNet50()

译文:```python

Initialize the model

model = ResNet50()


## 原文
{text}

Few-shot 的本质是用输入输出对来隐式定义格式和风格,比纯靠自然语言描述规则更可靠——模型从例子里"看"出来的模式,比从规则里"理解"出来的约束更稳。

2.3 长文本:分段 + 上下文窗口

本地模型上下文长度有限(4K / 8K / 32K 不等)。几万字的文档一次性塞进去必然截断。策略是分段翻译,每段带前一段的末尾做上下文:

你是一位专业翻译。以下是文档的第{n}段。
请根据前文语境,将本段翻译成{target_lang}。

## 前文末尾(用于保持语境连贯,不要翻译这部分)
{prev_tail}

## 本段原文
{chunk}

## 译文

prev_tail 取上一段的最后 2-3 句话即可,不用整段。这样既保持代词指代、术语一致性,又不会浪费上下文窗口。


三、代码调用:三种方式,从简单到生产级

以下代码假设你本地跑的是 Ollama(最常见)或任何 OpenAI 兼容 API(vLLM、llama.cpp server 都是)。Python 示例。

3.1 方式一:直接 HTTP 调用(最轻量,无依赖)

import requests

def translate(text: str, source_lang: str = "中文", target_lang: str = "英文") -> str:
    """最简翻译:直接调 Ollama 的 /api/chat 接口"""

    system_prompt = f"""你是一位专业的{source_lang}→{target_lang}翻译专家。

翻译规则:
1. 严格逐句翻译,不遗漏、不新增、不合并句子。
2. 保持原文段落结构和 Markdown 格式。
3. 代码块内容不翻译,只翻译注释。
4. 直接输出译文,不加任何解释或前后缀。"""

    response = requests.post(
        "http://localhost:11434/api/chat",
        json={
            "model": "qwen2.5:14b",
            "messages": [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": text},
            ],
            "stream": False,
        },
    )
    response.raise_for_status()
    return response.json()["message"]["content"]


# 使用
result = translate("大语言模型通过预测下一个token来生成文本。")
print(result)
# 输出: Large language models generate text by predicting the next token.

适用场景:快速验证、一次性脚本、不想装额外依赖。

缺点:没有重试、没有流式、没有 token 计数。生产环境不够用。

3.2 方式二:OpenAI SDK(推荐,兼容性最好)

vLLM、Ollama(开启 OpenAI 兼容模式)、llama.cpp server 都暴露 /v1/chat/completions 端点,可以直接用 openai 库:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",  # Ollama 的 OpenAI 兼容端点
    api_key="ollama",                       # 本地模型不需要真实 key,随便填
)

GLOSSARY = """
| 中文术语 | 英文翻译 |
|---------|---------|
| 大语言模型 | Large Language Model (LLM) |
| 提示词 | prompt |
| 微调 | fine-tuning |
| 推理 | inference |
"""

def translate(
    text: str,
    source_lang: str = "中文",
    target_lang: str = "英文",
    domain: str = "人工智能",
    glossary: str = "",
) -> str:
    """带术语表的翻译"""

    system_prompt = f"""你是一位专业的{source_lang}→{target_lang}翻译专家,擅长{domain}领域。

## 翻译规则
1. 严格逐句翻译,不遗漏、不新增、不合并句子。
2. 保持原文段落结构和 Markdown 格式标记。
3. 代码块内容不翻译,只翻译代码注释。
4. 专有名词按术语表翻译。
5. 直接输出译文,不加任何解释。"""

    if glossary:
        system_prompt += f"\n\n## 术语表\n{glossary}"

    response = client.chat.completions.create(
        model="qwen2.5:14b",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"## 原文\n{text}\n\n## 译文"},
        ],
        temperature=0.3,      # 翻译要确定性,温度调低
        max_tokens=4096,
    )

    return response.choices[0].message.content


# 使用
text = """
## 大语言模型简介

大语言模型通过**预测下一个token**来生成文本。常见的微调方法包括:

- LoRA:低秩适配,只训练少量参数
- QLoRA:量化 + LoRA,显存占用更低

```python
# 加载预训练模型
model = AutoModel.from_pretrained("qwen2.5-14b")

“”"

result = translate(text, glossary=GLOSSARY)
print(result)


输出大致是:

Introduction to Large Language Models

Large Language Models generate text by predicting the next token. Common fine-tuning methods include:

  • LoRA: Low-Rank Adaptation, only training a small number of parameters
  • QLoRA: Quantization + LoRA, lower GPU memory usage
# Load the pretrained model
model = AutoModel.from_pretrained("qwen2.5-14b")

注意几个参数选择:

- **`temperature=0.3`**:翻译是高确定性任务,温度越低输出越稳定。但不要设成 0(某些模型在 `temperature=0` 时行为异常),0.1~0.3 是甜点区。
- **`max_tokens=4096`**:必须显式设置。默认值在某些实现里很小(256 / 1024),长文本会被截断而你浑然不觉——**这是最常见的"翻译结果莫名其妙短了一截"的原因**。
- **术语表用 Markdown 表格**:模型对表格格式的理解优于纯文本列表,对齐更准确。

### 3.3 方式三:流式输出 + 分段处理(生产级)

实际场景里你要翻译的可能是整本书。需要:流式输出(用户能看到进度)、分段处理(绕过上下文限制)、结果拼接。

```python
from openai import OpenAI
from typing import Iterator

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

def chunk_text(text: str, max_chars: int = 2000) -> list[str]:
    """按段落切分文本,每段不超过 max_chars 字符。
    尽量在段落边界切,不要从句子中间断开。"""
    paragraphs = text.split("\n\n")
    chunks, current = [], ""

    for para in paragraphs:
        if len(current) + len(para) > max_chars:
            if current:
                chunks.append(current)
            current = para
        else:
            current = current + "\n\n" + para if current else para

    if current:
        chunks.append(current)
    return chunks


def translate_stream(
    text: str,
    source_lang: str = "中文",
    target_lang: str = "英文",
    max_chars: int = 2000,
) -> Iterator[str]:
    """流式分段翻译,逐块 yield 译文文本。"""

    chunks = chunk_text(text, max_chars)
    prev_tail = ""

    for i, chunk in enumerate(chunks):
        system_prompt = f"""你是一位专业的{source_lang}→{target_lang}翻译专家。

翻译规则:
1. 严格逐句翻译,不遗漏、不新增。
2. 保持段落结构和 Markdown 格式。
3. 代码块内容不翻译,只翻译注释。
4. 直接输出译文。"""

        # 第一段不需要前文上下文
        if prev_tail:
            system_prompt += f"\n\n## 前文末尾(保持语境连贯,不要翻译此部分)\n{prev_tail}"

        user_content = f"## 本段原文(第{i+1}/{len(chunks)}段)\n{chunk}\n\n## 译文"

        stream = client.chat.completions.create(
            model="qwen2.5:14b",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_content},
            ],
            temperature=0.3,
            max_tokens=4096,
            stream=True,
        )

        chunk_result = ""
        for event in stream:
            delta = event.choices[0].delta.content
            if delta:
                chunk_result += delta
                yield delta  # 实时吐给调用方

        # 保存末尾用于下一段的上下文
        sentences = chunk_result.rstrip().split("。")
        prev_tail = "。".join(sentences[-3:])


# 使用:实时打印翻译进度
long_text = open("article.md", encoding="utf-8").read()

for piece in translate_stream(long_text):
    print(piece, end="", flush=True)

几个设计决策的解释:

  • chunk_text 按段落切而不是按字数硬切:从句子中间断开会导致模型翻译出不完整的句子,拼接时出现断裂。按 \n\n(段落边界)切,最多在超长单段时才退化。
  • prev_tail 取最后 3 句:解决代词指代问题。比如第一段提到"该模型",第二段说"它的性能"——如果第二段没有上下文,"它"可能被翻成 “it” 而没有明确指代。带上前文末尾,模型能保持指代一致。
  • 流式 yield delta:调用方可以实时写文件 / 推前端 / 显示进度条,不用等全部翻完。

四、提示词调优的几个实战经验

4.1 System Prompt vs User Prompt:放哪都行,但有细微差别

翻译规则放 System Prompt 是惯例,好处是:

  • 多轮调用时不用重复传(部分框架会缓存 system prompt)。
  • 语义上更"权威"——模型对 system 的遵循度略高于 user。

但实测下来,对于翻译这种单轮任务,放 system 还是放 user 影响不大。真正影响结果的是规则的内容,不是位置。别在这上面纠结太久。

4.2 “直接输出译文” 这句话不能省

不加这句,模型有概率输出:

好的,我来帮您翻译这段内容:

[译文]

希望这个翻译对您有帮助!

这些前后缀在拼接长文本时是灾难——每段都带一句"好的",最终文档里全是"好的"。加一句"直接输出译文,不加任何解释或前后缀"能消除 95% 的情况。剩下 5% 靠代码后处理 strip 掉。

4.3 术语表的格式:表格 > 列表 > 纯文本

同样一份术语表,三种写法效果有差异:

纯文本(最差):

大语言模型翻译为Large Language Model,提示词翻译为prompt

列表(中等):

- 大语言模型 → Large Language Model
- 提示词 → prompt

表格(最好):

| 中文 | 英文 |
|------|------|
| 大语言模型 | Large Language Model |
| 提示词 | prompt |

原因猜测:训练数据里表格天然就是"映射关系"的结构,模型对这种格式的"查表"行为对齐得更好。这不是严格证明,是多次对比后的经验。

4.4 温度的选择

任务类型 temperature 原因
技术文档翻译 0.1 - 0.3 要确定性,术语统一
文学翻译 0.5 - 0.7 需要一定创造性,但不能跑偏
口语 / 对话翻译 0.3 - 0.5 自然但不过度发挥
术语提取 / 摘要 0.0 - 0.1 纯提取,零创造

翻译不是越低越好。文学翻译用 0.1 的温度,出来的句子干巴巴的,"她眼中闪过一丝不易察觉的哀伤"变成 “A trace of sadness flashed in her eyes”——准确但没味道。适当提高温度让模型有选词空间。


五、一个完整的可复用脚本

把上面的东西整合成一个开箱即用的命令行工具:

#!/usr/bin/env python3
"""本地 LLM 翻译工具
用法: python translate.py input.md --target en --output output.md
"""

import argparse
from pathlib import Path
from openai import OpenAI


def build_prompt(source_lang: str, target_lang: str, domain: str, glossary: str) -> str:
    prompt = f"""你是一位专业的{source_lang}→{target_lang}翻译专家,擅长{domain}领域。

## 翻译规则
1. 严格逐句翻译,不遗漏、不新增、不合并句子。
2. 保持原文段落结构和 Markdown 格式标记。
3. 代码块内容不翻译,只翻译代码注释。
4. 专有名词按术语表翻译。
5. 直接输出译文,不加任何解释或前后缀。"""
    if glossary:
        prompt += f"\n\n## 术语表\n{glossary}"
    return prompt


def chunk_text(text: str, max_chars: int = 2000) -> list[str]:
    paragraphs = text.split("\n\n")
    chunks, current = [], ""
    for para in paragraphs:
        if len(current) + len(para) > max_chars:
            if current:
                chunks.append(current)
            current = para
        else:
            current = current + "\n\n" + para if current else para
    if current:
        chunks.append(current)
    return chunks


def translate_file(input_path, output_path, source_lang, target_lang,
                   domain, glossary_path, model, base_url):
    client = OpenAI(base_url=base_url, api_key="local")

    glossary = ""
    if glossary_path and Path(glossary_path).exists():
        glossary = Path(glossary_path).read_text(encoding="utf-8")

    system_prompt = build_prompt(source_lang, target_lang, domain, glossary)
    text = Path(input_path).read_text(encoding="utf-8")
    chunks = chunk_text(text)

    results = []
    prev_tail = ""

    for i, chunk in enumerate(chunks):
        sp = system_prompt
        if prev_tail:
            sp += f"\n\n## 前文末尾(不要翻译此部分)\n{prev_tail}"

        resp = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": sp},
                {"role": "user", "content": f"## 原文\n{chunk}\n\n## 译文"},
            ],
            temperature=0.3,
            max_tokens=4096,
        )
        translated = resp.choices[0].message.content
        results.append(translated)

        sentences = translated.rstrip().split("。")
        prev_tail = "。".join(sentences[-3:])

        print(f"  [{i+1}/{len(chunks)}] 完成", file=sys.stderr)

    Path(output_path).write_text("\n\n".join(results), encoding="utf-8")
    print(f"翻译完成 → {output_path}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="本地 LLM 翻译工具")
    parser.add_argument("input", help="输入文件路径")
    parser.add_argument("--target", default="英文", help="目标语言")
    parser.add_argument("--source", default="中文", help="源语言")
    parser.add_argument("--output", help="输出文件路径")
    parser.add_argument("--domain", default="通用", help="领域")
    parser.add_argument("--glossary", help="术语表文件路径")
    parser.add_argument("--model", default="qwen2.5:14b", help="模型名称")
    parser.add_argument("--base-url", default="http://localhost:11434/v1")
    args = parser.parse_args()

    output = args.output or f"{args.input}.translated.md"
    translate_file(
        args.input, output, args.source, args.target,
        args.domain, args.glossary, args.model, args.base_url,
    )

用法:

# 基本用法
python translate.py article.md --target 英文

# 带术语表
python translate.py paper.md --target 英文 --domain 人工智能 --glossary terms.md

# 指定模型
python translate.py doc.md --target 英文 --model llama3.1:8b

术语表文件 terms.md 的格式:

| 中文 | 英文 |
|------|------|
| 大语言模型 | Large Language Model (LLM) |
| 提示词 | prompt |
| 微调 | fine-tuning |
| 零样本 | zero-shot |
| 思维链 | Chain-of-Thought (CoT) |

六、常见坑与排查

现象 原因 解决
译文莫名其妙短了一截 max_tokens 默认值太小 显式设 max_tokens=4096 或更大
每段前面带"好的,以下是翻译" 没加"直接输出"约束 提示词末尾加"直接输出译文,不加解释"
代码块里的字符串被翻译了 没加"代码块不翻译"规则 加规则 + few-shot 示例
术语前后不一致 没用术语表,或术语表太长被忽略 用表格格式术语表,控制在 20 条以内
长文档代词指代丢失 分段时没带上下文 加 prev_tail 前文末尾
翻译速度极慢 模型太大 / 没开流式 / 逐段串行 用 7B-14B 模型,开 stream,或并发翻译多段
格式标记被破坏 没明确说"保持 Markdown 格式" 加"保持原文 Markdown 格式标记不变"

最后一个值得展开:并发翻译多段。如果你不在乎段间上下文连贯性(比如每段都是独立的技术说明),可以把分段后的 chunk 用 concurrent.futures 并发扔给模型,速度提升 N 倍(N = 并发数)。但如果段间有"它""该模型"这种指代,并发会导致指代丢失——这时候老老实实串行。

from concurrent.futures import ThreadPoolExecutor

def translate_concurrent(chunks: list[str], system_prompt: str) -> list[str]:
    """并发翻译(仅适用于段间无指代依赖的场景)"""
    def translate_one(chunk: str) -> str:
        resp = client.chat.completions.create(
            model="qwen2.5:14b",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": chunk},
            ],
            temperature=0.3,
            max_tokens=4096,
        )
        return resp.choices[0].message.content

    with ThreadPoolExecutor(max_workers=4) as pool:
        return list(pool.map(translate_one, chunks))

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。