事件背景
事情的起因是最近销售那边忽然拉来了个日本客户, 并想以此打开日本市场,大家都很开心的准备国际化的相关事宜,然后就有个事情卡着了。
我们的资源包没有日文的版本,公司联系之前的渠道,资源文件给过去,因为时间比较紧词条比较多,按照人家的流程核算了一下, 竟然要 100,000 块(人民币),这个比我们之前别的小语种语言翻译贵了不止一倍。
PS: 英文翻译我们产品部门自己做的,繁体中文、韩语、泰文、阿拉伯文等等语言都是和我们合作的翻译工作室做的。
日文翻译这个事情合计来合计去,最后落回到了研发部头上。下面我跟大家聊聊我怎么让大模型帮我把这个活干了。
翻译的方案
方案一:机器翻译
提到翻译,我们首先想到的肯定是机器翻译,比如国内的百度翻译、有道翻译等,国外的谷歌翻译、DeepL翻译等。
但是自动的机器翻译有个问题,多义词经常翻译出错,这个在专业文档里面经常出现,经常会被翻译成统一词汇。
比如: Performance 这个单词,在技术文档里面一般是 性能 的意思, 但是在机器翻译没有上下文(比如作为标题)的时候常常被翻译成演出。
而我们翻译软件的UI恰恰是很多没有上下文句子的单词或者短语。所以机器翻译不是很好的一个选择。
除了多义词,专业领域的词汇经常会有一些专业的叫法,或者是某个软件独有的叫法。这种情况也和通常的翻译不一样,设置中文都要跟人解释了大家才能明白,比如托盘,踢单。这些也是机器翻译不能处理的。
ps: 当前机器翻译支不支持行业接口和词汇表等等设定,我还没调研过哈,暂且当不支持吧。
方案一:人工翻译
专业的事情交给专业的人,所以由专业人士翻译是最靠谱的,不过,也为专业人士翻译是最好的。
我们拿着这次翻译的数据给经常合作的供应商看了之后,三万左右的词语和句子要大概 10,000 块, 没错就是 10W 块,对方也觉得 10W 块翻译这点儿东西有点儿贵,但是人家的核算方法费用算下来就这么多。
在这个经济不咋好的年份,10W 搞个软件多语言,公司也不太想花这个钱。
方案三:大模型翻译
如果是之前我们可能就硬着头皮找翻译公司去翻译了。不过在 2023 年,大语言模型横空出世的年代,我们有了第三个选择:大语言模型。
前面我们说过,中文也有很多大模型,智谱、百川、百度等等。
我们还是先问问最熟悉的智谱AI,依然使用 ChatGLM-Turbo 这个模型:
看看他能不能胜任翻译这个工作。
好的,它是懂日文的,这个就好说。时间原因,再说我也评估不出来 LLM 日文能力的好坏,就直接用智谱AI。
LLM 翻译方案
翻译的整体方案大概如下:
资源文件
数据格式
资源文件我这里用 csv 格式的数据,配合 Pandas 这个 Python 库的 DataFrame,能够方便的进行数据处理。
文件里面有中文、日文、英文三列,分别是 简体中文,English,日本語/にほんご。
| 简体中文 | English | 日本語/にほんご |
| 规则 | rules | ルール |
| 参数 | parameters | パラメータ |
| 质检 | quarantine | 品質チェック |
| 任务号 | mission number |
没有翻译的地方日文这个位置为空。
Pandas 读取数据
import pandas as pd
df = pd.read_csv(csv_file_path)
Pandas 保存数据为 CSV 文件
import pandas as pd
df.to_csv(file_to_save, index=False)
Streamlit 显示 Pandas 数据
import streamlit as st
st.dataframe(st)
分批调用
文案文字数量大概是 10W 这个量级的,超出了常规的 Token 数量限制,调用 LLM 接口肯定不能一次翻译完成。就需要把资源文件拆分成一段段的进行翻译,智谱AI的 Token 限制可能是 4096(从UI上看到的,没有找到文档说明)。
每个批次的大小可以根据实际情况进行调整,比如 80 。
结果保存
结果我们也就保存为 CSV 文件,直接覆盖之前的文件,保证格式的统一。
使用大模型
接下来就是用大模型进行翻译。
提示工程
第一步当然是编写提示语,随着大模型越来越聪明,我们编写提示语也变得越来越简单。
零样本提示
然后我们看下返回:
效果还真不赖。
LLM 的一些问题
我接连试了几次,还真好。不过当我用程序调用的时候,偶尔会出现奇怪的错误,比如Json格式不正确:
{
"订单号": "注文番号",
"订单日期": "注文日",
"订单状态": "注文状態",
"订单金额": "注文金额",
"订单商品": "注文商品",
"订单客户": "注文顧客",
]
注意后面这个字符,它是竟然是一个 ],
但是这个问题我在其网页版本的体验中心却没有重试出来这个问题。
无奈之下我给提示语加上一个:
过了一会儿,他又出现了一个别的问题,返回的数据大概是这样:
{
"订单号": "注文番号",
"订单日期": "注文日",
"订单状态": "注文状態",
"订单金额": "注文金额",
"订单商品": "注文商品",
"订单客户": "注文顧客",
]
不知道你们看出问题了没,刚开始每行末尾的逗号是英文半角 ,的,从某一行开始,他的逗号变成中文全角逗号 , 了, 我也不知道咋回事。反正就会这样。
少样本提示
当然你也可以用少样本提示,主要原因是昨天我的零样本提示他根本不返回正确的数据。
比如还是那个提示语:
他会给你这样的返回:
[
{"Chinese":"订单号", "Japanese":"注文番号"},
{"Chinese":"订单日期", "Japanese":"注文日"},
{"Chinese":"订单状态", "Japanese":"注文状態"},
{"Chinese":"订单金额, "Japanese":"注文金额"}
]
总之就是你也不能说他错,但是就主打一个随意,每次接错又不一样。
我当时的少样本提示大概是这样的,比较简单直接在 Prompt 里面写:
你作为供应链行业日文翻译助手,帮我进行供应链行业的专业术语从中文到日文的翻译。
输入格式是 JSON 对象,把 Json Key 翻译成日文,输出格式是对应翻译好的 JSON 格式。
当输入:
{
"车号": "",
"续重": "",
"任务号": ""
}
返回:
{
"车号": "車号",
"续重": "続重",
"任务号": "タスク番号"
}
请翻译一下内容。
也基本没啥问题,效果和之前一样。
使用多轮对话
上篇文章我们提过,chatglm_turbo 接口原生就支持多轮对话,我最后用多轮对话的方式来实现少样本提示:
prompt_template = [
{"role": "user",
"content": """你作为供应链行业日文翻译助手,帮我进行供应链行业的专业术语从中文到日文的翻译。输入格式是 JSON 对象,把 Json Key 翻译成日文,输出格式是对应翻译好的 JSON 格式。
请一定认真校验返回数据 JSON 格式的正确性。"""},
{"role": "assistant", "content": "好的"},
{"role": "user", "content": """{
"车号": "",
"续重": "",
"任务号": ""
}"""},
{"role": "assistant", "content": """{
"车号": "車号",
"续重": "続重",
"任务号": "タスク番号"
}"""},
{"role": "user", "content": """{
"签收": "",
"发货": "",
"收货": "",
"包裹": "",
"重量": ""
}"""},
{"role": "assistant", "content": """{
"签收": "署名を受け取る",
"发货": "発送",
"收货": "受け取り",
"包裹": "小包",
"重量": "重量"
}"""},
]
Prompt 的问题我们就聊到这儿,整体来说 LLM 还是很不错的。
界面
让大模型帮我们干活,界面当然少不了,我们仍然使用 Streamlit 来做界面,又简单又快速又没关。
侧边栏
首先还是侧边栏,我们先在顶部放个图片当做应用的标题:
import streamlit as st
with st.sidebar:
st.image('assets/logo-title.png', use_column_width=True)
st.subheader('', divider='rainbow')
然后给个输入框可以自定义 API Key 如果没有就用系统环境变量的。以及一个支持多选的上传文件的按钮。
with st.sidebar:
...
st.text_input('智谱API Key', key='api-key', type='password')
uploaded_files = st.file_uploader("Choose CSV files to translate", accept_multiple_files=True)
效果如下:
选择文件后的效果:
当选择文件后,我们给出一个「翻译」按钮,用 if 判断只有当上传了文件才显示:
with st.sidebar:
...
uploaded_files = st.file_uploader("Choose CSV files to translate", accept_multiple_files=True)
if uploaded_files:
st.button('Translate', use_container_width=True, type="primary")
显示上传文件数据
既然做 UI,那么尽量让数据可视,在主区域用多Tab的方式显示一下上传文件的数据。程序如下:
先定义变量。
# 上传文件的目录
upload_path = '/tmp/uploads/'
os.makedirs(upload_path, exist_ok=True)
# 多Tab缓存
st.session_state.data_containers = {}
然后在上传之后直接用多 Tab 数据表格把数据显示出来。
if uploaded_files:
# 显示上传成功的提示
with st.sidebar:
st.info('Uploaded %s csv files!' % len(uploaded_files))
# 使用聊天的UI样式呈现
with st.chat_message("assistant"):
tab_names = [f.name for f in uploaded_files]
tabs = st.tabs(tab_names)
for index, uploaded_file in enumerate(uploaded_files):
# 获取文件内容
bytes_data = uploaded_file.read()
tempfile = os.path.join(upload_path, uploaded_file.name)
# 保存文件
with open(tempfile, 'wb') as f:
f.write(bytes_data)
df = pd.read_csv(tempfile)
# 用表格显示文件内容
with tabs[index]:
empty = st.empty()
empty.dataframe(df, use_container_width=True)
st.session_state.data_containers[index] = empty
效果如下:
Token 调用的显示
翻译的过程中我还想看一看 Token 的使用情况,来大致估算下费用。在侧边栏增加几个 Metric 来试试更新 Token 的使用。
实现上面效果,我们先定义一个 ZhipuUsage 类,并创建一个对象:
class ZhipuUsage:
prompt = 0
completion = 0
total = 0
# 创建 Usage 对象
zhipu_usage = ZhipuUsage()
在每次接口返回的时候更新下统计信息:
# 获取接口返回的用量
usage = response['data']['usage']
# 更新用量
zhipu_usage.prompt += usage['prompt_tokens']
zhipu_usage.completion += usage['completion_tokens']
zhipu_usage.total += usage['total_tokens']
然后在上传成功的提示下面增加一个容器,并调用更新方法显示 Usage 用量:
with st.sidebar:
st.info('Uploaded %s csv files!' % len(uploaded_files))
st.subheader('Token Usage')
metrics1 = st.empty()
update_metrics()
update_metrics 方法如下:
def update_metrics():
col1, col2, col3 = metrics1.columns(3)
col1.metric("Prompt", zhipu_usage.prompt)
col2.metric("Completion", zhipu_usage.completion)
col3.metric("Total", zhipu_usage.total)
同样的方法定义一个 翻译结果的展示类:
class MetricsTranslate:
success = 0
fail = 0
total = 0
展示下翻译情况:
A,他给你翻译了
B,这些异常情况都是我在调用过程中遇到的, 我们需要对这些异常进行
兼容处理,记录错误,并进行异常数据重试。
翻译进行式
前面所有的准备工作都做好了,后面的翻译就好说了。
分批翻译
分批翻译的主要步骤如下:
• 依次读取所有文件
• 每个文件一行一行的读取
• 到达一个批次的时候调用 LLM 接口
• 解析接口数据
• 记录翻译结果
• 记录异常数据
• 更新翻译进度
• 文件读完的时候把剩余数据翻译一下
先定义一下翻译方法:
def translate_by_llm_and_save():
prompt = make_prompt(batch_sentences)
response = invoke_prompt(prompt)
try:
json_response = json.loads(response)
return json_response
except:
return {}
然后就是实现翻译的流程,程序不复杂,但是代码缩进的层次多了点儿。
if translate_button:
with st.chat_message("assistant"):
with st.spinner("正在努力翻译中,别着急,马上就好 :lollipop: ... "):
for index, uploaded_file in enumerate(uploaded_files):
progress_text = "%s" % uploaded_file.name
progress = st.progress(0, text=progress_text)
df = st.session_state.data_frames[index]
word_idx = {}
batch_sentences = []
TRANSLATE_BATCH_SIZE = 1
for idx, row in df.iterrows():
progress.progress(idx / len(df.index), text=progress_text)
chinese = str(row[COL_CHINESE]).strip()
japanese = str(row[COL_JAPANESE]).strip()
if japanese == None or japanese == 'nan' or japanese == '':
word_idx[chinese] = idx
batch_sentences.append(chinese)
if len(word_idx.keys()) >= TRANSLATE_BATCH_SIZE:
translate_by_llm_and_save(df)
word_idx = {}
batch_sentences = []
st.session_state.data_containers[index].dataframe(
df,
use_container_width=True
)
if len(word_idx.keys()) > 0:
translate_by_llm_and_save(df)
progress.progress(1.0, text=progress_text)
st.success('已经全部翻译完成!')
st.balloons()
我大致讲一下逻辑,首先再翻译按钮点击的时候,提示一下正在翻译
然后循环翻译文件,并显示翻译进度:
progress_text = "%s" % uploaded_file.name
progress = st.progress(0, text=progress_text)
并且在翻译过程中更新进度:
progress.progress(idx / len(df.index), text=progress_text)
每次翻译成功之后把翻译成功的数据更新回 UI。
st.session_state.data_containers[index].dataframe(
df,
use_container_width=True
)
因为翻译过程比较长,我们顺便记录一下当前正在翻译的数据,以及翻译失败的数据:
PS: 文章中的数据都是虚拟数据哦
最后给大家看看完整的效果:
后记
这次翻译的工程还有一些东西文章中没写:
• 中文和英文加上字典一起作为 Prompt 会得到更好的效果
• 翻译之后还需要机器和人工校验,也可以再把日文结果翻译成中文和英文进行二次校验
• 实际翻译耗时还是挺久的,每次调用可能都在半分钟以上(瞎估计的,但肯定没有视频中那么快),可能换成 SSR 调用会好,
• 另外大规模翻译还是比较耗 Token 的,不过好在智谱送了不少 Token。
这次我没有用 OpenAI 的 API 做对比,直接用上面的方案当成生成使用的,国内的大模型现阶段可能和国外的还有一些差距, 但是鉴于大模型越用越好的特性,只要大家多多使用,我相信他们能很快满足我们的生产需求的。

