本篇文章,我们聊聊如何使用 LLM IDE (Dify) 快速搭建一个模型应用,以及使用超长上下文的 200K 模型,完成懒人式的电子书翻译。

准备工作

最近在 GitHub 上看到了前 HuggingFace 员工,前 transformers 核心贡献者之一的 Stas Bekman开源的方式写了一本机器学习的书,基于之前训练 BLOOM 176B 和 IDEFICS 80B 的经验,相对详细的聊了训练大语言模型和多模态模型。

这本书的干货还是蛮多的,我个人认为或许能够对和我一样的模型爱好者有帮助,所以我动了翻译它,分享给同好的心思。

翻译完毕的内容,开源在了 soulteary/ml-engineering,欢迎一键三连,同样欢迎一起协作,让内容更精致,提升大家的阅读体验。

作为一个懒人,自然是选择目前最高效的方案来整啦。我主要借助了两个外部工具来做这个事情:

第一个,是前两周申请到的零一万物的模型(yi-34b-chat-200k),能够将作者每一章的全文都扔到模型里,而不用切分章节或做一些递归式的章节摘要等麻烦事。

第二个,是开源生态做的非常好的 LLM 应用开发平台(LLM IDE),Dify。使用它可以快速把 Prompt 调试好,然后启动一个能够按照预期调用接口,拿到数据的应用。

前者的 API 申请,需要去官方社区填写申请表,后者是完全开源的工具,可以从 GitHub langgenius/dify 获取应用代码和快速启动配置。

另外,我特别建议你准备一杯饮料或者一本书,在模型开始翻译内容后,我们可以喝着饮料看看书,放松放松。

准备工作:获取要翻译的电子书

获取电子书比较简单,我们只需要使用 git 下载它就行,为了方便修改文件,我对原始仓库进行了 fork

git clone https://github.com/soulteary/ml-engineering.git

包含电子书“源码”的内容下载完毕,我们先放在一边,稍后使用。

准备工作:启动 Dify IDE

我们有两种方案使用 Dify,可以选择使用 Dify 的 Cloud 在线版本,也可以本地使用 Docker 快速地搭建应用。

前者没什么好聊的,使用 GitHub 或者 Google 账号授权登录,默认“入门套餐”免费送 10 个应用的额度。后者的话,我们需要下载程序:

# git clone https://github.com/langgenius/dify.git                                                                                                               
Cloning into 'dify'...
remote: Enumerating objects: 44636, done.
remote: Counting objects: 100% (7874/7874), done.
remote: Compressing objects: 100% (1528/1528), done.
remote: Total 44636 (delta 6730), reused 7116 (delta 6326), pack-reused 36762
Receiving objects: 100% (44636/44636), 24.19 MiB | 10.31 MiB/s, done.
Resolving deltas: 100% (31710/31710), done.

然后,使用 docker 启动应用:

cd dify/docker
docker compose up -d

默认的配置中包含了下面几种应用服务:

  • dify,应用的前后端服务。
  • postgres 作为数据库使用。
  • redis 作为缓存服务使用。
  • weaviate 作为向量数据库使用。
  • nginx 作为应用网关使用,默认端口是 80,我们可以根据自己的需求修改调整。

应用启动完毕,设置完默认的管理员邮箱和密码,我们就能够看到 Dify 的 Dashboard 啦。

Dify Dashboard

准备工作:配置零一万物模型

当在社区里申请了零一万物的模型 API 后,邮箱中会 “New” 出一份包含 “API Key” 和对应使用文档的邮件。

点击 Dify 控制台右上角的个人头像,在下拉菜单中选择“设置”,能够看到 Dify 支持的模型列表。

支持的模型列表

然后在打开的“模型供应商”窗口中往下滑到底。直到看到 “OpenAI 兼容 API” 的项目,点击它,选择添加一个模型后端服务。

OpenAI API 兼容的模型

然后,使用邮件和模型文档中的内容完成模型的配置即可。

配置 Yi-34B-Chat-200K 模型

点击确认,界面右上角会提醒我们配置完毕,界面中也会出现 yi-34b-chat-200k 的可用模型提示。

模型后端配置完毕

准备工作都结束后,我们就可以来配置模型应用,以及使用 AI 写一些程序,进行电子书的翻译啦。

配置模型翻译应用

回到 Dify 的 Dashboard,选择创建应用。

创建模型应用

然后,给应用起一个合适的名字(主要方便我们自己区分多个应用)。

起一个合适的名字

应用创建完毕后,会弹出模型超参数设置的窗口,我们适当调整下上下文长度即可。(目前 200K 模型调用时,我们需要稍微减少一些 Token,避免请求参数中各种字符加上去,超出接口限制)

适当调整模型的窗口上下文

设置完毕,我们将要翻译的长文内容随便找一篇扔到输入框中进行测试即可。相比较手动拼接字符串,调整 Prompt,使用 Dify 这类 LLM IDE 效率上还是提升蛮多的。

使用待测试内容进行测试

点击运行按钮,在下面的结果区域就能够看到模型在迅速地吐结果啦。

验证模型配置正确

再次回到 Dashboard,左侧的侧边栏就会出现我们刚刚配置的“电子书翻译器”,点击应用名称,我们就能够看到一个简易的模型应用页面啦。我们只需要在左侧选择要翻译到的目标语言,以及输入我们要翻译的内容,点击i运行,就能够得到模型的输出结果了。

翻译一篇内容

当然,Dify 也支持批量翻译。

翻译多篇内容

当然,如果文章只是聊如何使用 Dify 折腾一个界面,那么这篇文章也太水了,毕竟这样不能自动化。让我们继续折腾吧。

编写基于模型后端的自动化翻译程序

让我们继续发挥懒人精神,借助 AI 模型,来编写一个能够批量、自动翻译内容的工具吧。

虽然在上文中,我们能够看到模型具备翻译能力,通过不断地“复制粘贴”内容到上面在 Dify 配置好的模型应用也能搞定翻译。

但是,通常情况下,我们想翻译的这类电子书往往会有许多文件需要被处理。以及后续往往不确定哪一个文件又会被作者更新,维护成本还是蛮高的。

所以,如果我们编写一个简单的程序,能够借助模型能力,始终对项目进行完整翻译,或许这个事情就会变的很简单:

  • 程序能够读取目录中的每一个需要翻译的文件
  • 将文件内容传递给模型程序获取翻译结果
  • 然后将翻译好的内容更新到原始文件中

如果后续原作者有其他的更新,我们也只需要按需执行一遍上面的程序,就能够获得最新的资料的翻译内容啦。

编译模型 API 调用程序

让我们先来编写最重要的模型翻译程序,让程序能够调用上面我们配置好的应用,来进行翻译工作。

因为 dify 目前 Golang SDK 存在一些问题,不能直接使用,所以我们先曲线救国,借助 AI 来封装一个简单的调用工具。

打开上面的模型应用界面,在浏览器页面右键打开“开发者工具”,然后随便输入一些内容,点击运行按钮,让模型进行翻译。此时我们能够在“开发者工具”的“网络”选项卡中捕获到模型的程序请求。选中请求,在请求上右键选择“复制”,将请求以 curl 的方式保存。

在浏览器“开发者工具”中复制模型调用请求

接着,在已经对接好模型的 Chat 工具里,将上面复制保存下来的请求粘贴进去,并在最开头写“一句要求”:

将下面的 curl 请求改写为 golang 程序:

'''bash
curl 'https://dify.black.com/api/completion-messages' \
  -H 'authority: dify.black.com' \
  -H 'accept: */*' \
  -H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7' \
...
  --data-raw '{"inputs":{"target_language":"Chinese","query":"hello world"},"response_mode":"streaming"}'
'''

稍等片刻,模型就会帮助我们写好类似下面的一段程序:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
)

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`{"inputs":{"target_language":"Chinese","query":"hello world"},"response_mode":"streaming"}`)
	req, err := http.NewRequest("POST", "https://dify.black.com/api/completion-messages", data)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("authority", "dify.black.com")
	req.Header.Set("accept", "*/*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7")
	req.Header.Set("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZTAxZmU0OC05OTRjLTQ3MGItODgxMS0zNTNhNjlhY2ZhZDciLCJzdWIiOiJXZWIgQVBJIFBhc3Nwb3J0IiwiYXBwX2lkIjoiYmUwMWZlNDgtOTk0Yy00NzBiLTg4MTEtMzUzYTY5YWNmYWQ3IiwiYXBwX2NvZGUiOiJHV3JDeDBsYjNaQTNFU3hOIiwiZW5kX3VzZXJfaWQiOiI0ZGM4MmFmOC02OTY3LTRkYzctYTFkMS00YmU5MGUzNGY5N2EifQ.jVewI-mI-p-vybJ66yep1-sPYbVUuSHUPSboOSDRxbE")
	req.Header.Set("cache-control", "no-cache")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("dnt", "1")
	req.Header.Set("origin", "https://dify.black.com")
	req.Header.Set("pragma", "no-cache")
	req.Header.Set("referer", "https://dify.black.com/completion/GWrCx0lb3ZA3ESxN")
	req.Header.Set("sec-ch-ua", `"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"`)
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", `"macOS"`)
	req.Header.Set("sec-fetch-dest", "empty")
	req.Header.Set("sec-fetch-mode", "cors")
	req.Header.Set("sec-fetch-site", "same-origin")
	req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", bodyText)
}

根据小学二年级学会的编程技巧,我们将上面的程序做一些简化,去掉可加可不加的内容,让程序先看起来比较干净:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
)

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`{"inputs":{"target_language":"Chinese","query":"hello world"},"response_mode":"streaming"}`)
	req, err := http.NewRequest("POST", "https://dify.black.com/api/completion-messages", data)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("authority", "dify.black.com")
	req.Header.Set("accept", "*/*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7")
	req.Header.Set("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZTAxZmU0OC05OTRjLTQ3MGItODgxMS0zNTNhNjlhY2ZhZDciLCJzdWIiOiJXZWIgQVBJIFBhc3Nwb3J0IiwiYXBwX2lkIjoiYmUwMWZlNDgtOTk0Yy00NzBiLTg4MTEtMzUzYTY5YWNmYWQ3IiwiYXBwX2NvZGUiOiJHV3JDeDBsYjNaQTNFU3hOIiwiZW5kX3VzZXJfaWQiOiI0ZGM4MmFmOC02OTY3LTRkYzctYTFkMS00YmU5MGUzNGY5N2EifQ.jVewI-mI-p-vybJ66yep1-sPYbVUuSHUPSboOSDRxbE")
	req.Header.Set("content-type", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", bodyText)
}

将上面的内容保存为 main.go,然后执行 go run main.go,因为上面程序的请求中包含了 "response_mode":"streaming",所以我们将得到类似下面的流式输出的结果:

# go run main.go

data: {"event": "message", "id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "task_id": "28591fdc-9ec2-4f12-b931-86e2dba66d25", "message_id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "answer": "\u4f60\u597d", "created_at": 1710262616}

data: {"event": "message", "id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "task_id": "28591fdc-9ec2-4f12-b931-86e2dba66d25", "message_id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "answer": "\uff0c", "created_at": 1710262616}

data: {"event": "message", "id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "task_id": "28591fdc-9ec2-4f12-b931-86e2dba66d25", "message_id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "answer": "\u4e16\u754c", "created_at": 1710262616}

data: {"event": "message", "id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "task_id": "28591fdc-9ec2-4f12-b931-86e2dba66d25", "message_id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "answer": "\uff01", "created_at": 1710262616}

data: {"event": "message", "id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "task_id": "28591fdc-9ec2-4f12-b931-86e2dba66d25", "message_id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "answer": "", "created_at": 1710262616}

data: {"event": "message_end", "task_id": "28591fdc-9ec2-4f12-b931-86e2dba66d25", "id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "message_id": "72e9eda4-d251-4506-9f50-06cad6db55fa", "metadata": {}}

在我们的场景中,处理多帧的流式输出比较麻烦(浪费代码行数),所以我们可以将流式输出参数去掉。我们再次执行程序,就能够得到传统的输出结果啦,这样更方便处理。

# go run main.go 
{"event": "message", "task_id": "c2355980-9685-450d-ad62-b4b95c50b6c4", "id": "b56c5ff7-c7c6-476e-a28c-5d57cc91ce2d", "message_id": "b56c5ff7-c7c6-476e-a28c-5d57cc91ce2d", "mode": "completion", "answer": "\u4f60\u597d\uff0c\u4e16\u754c\uff01", "metadata": {}, "created_at": 1710262788}

我们将上面的输出结果贴到模型对话窗口中,然后继续向模型提问,比如问模型“如何序列化上面的 JSON 内容,获得 answer 字段中的内容”,简单组合代码,可以得到类似下面的程序:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
)

type Response struct {
	Answer string `json:"answer"`
}

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`{"inputs":{"target_language":"Chinese","query":"hello world"}}`)
	req, err := http.NewRequest("POST", "https://dify.black.com/api/completion-messages", data)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("authority", "dify.black.com")
	req.Header.Set("accept", "*/*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7")
	req.Header.Set("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZTAxZmU0OC05OTRjLTQ3MGItODgxMS0zNTNhNjlhY2ZhZDciLCJzdWIiOiJXZWIgQVBJIFBhc3Nwb3J0IiwiYXBwX2lkIjoiYmUwMWZlNDgtOTk0Yy00NzBiLTg4MTEtMzUzYTY5YWNmYWQ3IiwiYXBwX2NvZGUiOiJHV3JDeDBsYjNaQTNFU3hOIiwiZW5kX3VzZXJfaWQiOiI0ZGM4MmFmOC02OTY3LTRkYzctYTFkMS00YmU5MGUzNGY5N2EifQ.jVewI-mI-p-vybJ66yep1-sPYbVUuSHUPSboOSDRxbE")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("origin", "https://dify.black.com")
	req.Header.Set("content-type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	var response Response
	json.Unmarshal(bodyText, &response)

	fmt.Println(response.Answer)
}

再次执行程序,我们将得到符合我们预期的结果,只包含模型处理结果的内容:

# go run main.go

你好,世界!

到这里为止,我们就完成了核心的基于模型的翻译程序。

为了方便后续的调用,我们可以再稍微调整下代码结构,以及完善函数传递参数的方式:

func query(input string) (result string, err error) {
	client := &http.Client{}

	type Request struct {
		Inputs struct {
			TargetLanguage string `json:"target_language"`
			Query          string `json:"query"`
		} `json:"inputs"`
		ResponseMode string `json:"response_mode,omitempty"`
	}

	type Response struct {
		Answer string `json:"answer"`
	}

	var payload Request
	payload.Inputs.TargetLanguage = "Chinese"
	payload.Inputs.Query = input

	payloadBuf, err := json.Marshal(payload)
	if err != nil {
		return result, err
	}

	var data = strings.NewReader(string(payloadBuf))
	req, err := http.NewRequest("POST", "https://dify.black.com/api/completion-messages", data)
	if err != nil {
		return result, err
	}

	req.Header.Set("authority", "dify.black.com")
	req.Header.Set("accept", "*/*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7")
	req.Header.Set("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZTAxZmU0OC05OTRjLTQ3MGItODgxMS0zNTNhNjlhY2ZhZDciLCJzdWIiOiJXZWIgQVBJIFBhc3Nwb3J0IiwiYXBwX2lkIjoiYmUwMWZlNDgtOTk0Yy00NzBiLTg4MTEtMzUzYTY5YWNmYWQ3IiwiYXBwX2NvZGUiOiJHV3JDeDBsYjNaQTNFU3hOIiwiZW5kX3VzZXJfaWQiOiI0ZGM4MmFmOC02OTY3LTRkYzctYTFkMS00YmU5MGUzNGY5N2EifQ.jVewI-mI-p-vybJ66yep1-sPYbVUuSHUPSboOSDRxbE")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("origin", "https://dify.black.com")
	req.Header.Set("content-type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		return result, err
	}

	var response Response
	json.Unmarshal(bodyText, &response)
	return response.Answer, nil
}

编写需要翻译的文本程序

接下来,我们来(让模型)编写文件更新相关的程序。还是和模型聊聊天,询问模型如何 “使用 golang 获取目录中所有的 .md 文件路径”,模型告诉我们这样就行:

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func visit(paths *[]string) filepath.WalkFunc {
	return func(path string, info os.FileInfo, err error) error {
		if err != nil {
			fmt.Printf("Error accessing path %q: %v\n", path, err)
			return err
		}
		if !info.IsDir() && filepath.Ext(path) == ".md" {
			*paths = append(*paths, path)
		}
		return nil
	}
}

func main() {
	var mdFiles []string
	root := "./" // 确定你想要开始搜索的目录路径

	err := filepath.Walk(root, visit(&mdFiles))
	if err != nil {
		panic(err)
	}

	for _, file := range mdFiles {
		fmt.Println(file)
	}
}

将上面的内容保存为 main.go,然后调整下要搜索的目录路径,执行 go run main.go 后,我们可以得到所有需要翻译的电子书文件路径:

# go run main.go 

../ml-engineering/README.md
../ml-engineering/build/README.md
../ml-engineering/compute/README.md
../ml-engineering/compute/accelerator/README.md
../ml-engineering/compute/accelerator/nvidia/debug.md
../ml-engineering/compute/cpu/README.md
../ml-engineering/compute/cpu-memory/README.md
../ml-engineering/debug/README.md
../ml-engineering/debug/make-tiny-models-tokenizers-datasets.md
../ml-engineering/debug/nccl-performance-debug.md
../ml-engineering/debug/pytorch.md
../ml-engineering/debug/tiny-scripts/README.md
...

接下来,我们再问问模型如何 “使用 golang 读取文件,在原始内容后添加新内容,写回文件”,很容易得到类似下面的程序代码:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 设置文件名
	filename := "example.txt"

	// 读取文件内容
	data, err := os.ReadFile(filename)
	if err != nil {
		fmt.Println("Error reading file:", err)
		return
	}

	// 需要添加的新内容
	newContent := "\nThis is the new content to append."

	// 将新内容追加到原始内容后
	data = append(data, newContent...)

	// 写回文件
	err = os.WriteFile(filename, data, 0644) // 0644 是文件权限 (通常意味着所有者可读写,组和其他用户只读)
	if err != nil {
		fmt.Println("Error writing to file:", err)
		return
	}

	fmt.Println("Content appended successfully")
}

到目前为止,我们所有需要的程序就都齐啦。

编写最终的翻译程序

我们将上面的两段程序代码,以及上文中我们封装好的模型翻译程序代码合并起来:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

func query(input string) (result string, err error) {
	client := &http.Client{}

	type Request struct {
		Inputs struct {
			TargetLanguage string `json:"target_language"`
			Query          string `json:"query"`
		} `json:"inputs"`
		ResponseMode string `json:"response_mode,omitempty"`
	}

	type Response struct {
		Answer string `json:"answer"`
	}

	var payload Request
	payload.Inputs.TargetLanguage = "Chinese"
	payload.Inputs.Query = input

	payloadBuf, err := json.Marshal(payload)
	if err != nil {
		return result, err
	}

	var data = strings.NewReader(string(payloadBuf))
	req, err := http.NewRequest("POST", "https://dify.black.com/api/completion-messages", data)
	if err != nil {
		return result, err
	}

	req.Header.Set("authority", "dify.black.com")
	req.Header.Set("accept", "*/*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7")
	req.Header.Set("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZTAxZmU0OC05OTRjLTQ3MGItODgxMS0zNTNhNjlhY2ZhZDciLCJzdWIiOiJXZWIgQVBJIFBhc3Nwb3J0IiwiYXBwX2lkIjoiYmUwMWZlNDgtOTk0Yy00NzBiLTg4MTEtMzUzYTY5YWNmYWQ3IiwiYXBwX2NvZGUiOiJHV3JDeDBsYjNaQTNFU3hOIiwiZW5kX3VzZXJfaWQiOiI0ZGM4MmFmOC02OTY3LTRkYzctYTFkMS00YmU5MGUzNGY5N2EifQ.jVewI-mI-p-vybJ66yep1-sPYbVUuSHUPSboOSDRxbE")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("origin", "https://dify.black.com")
	req.Header.Set("content-type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		return result, err
	}

	var response Response
	json.Unmarshal(bodyText, &response)
	return response.Answer, nil
}

func visit(paths *[]string) filepath.WalkFunc {
	return func(path string, info os.FileInfo, err error) error {
		if err != nil {
			fmt.Printf("Error accessing path %q: %v\n", path, err)
			return err
		}
		if !info.IsDir() && filepath.Ext(path) == ".md" {
			*paths = append(*paths, path)
		}
		return nil
	}
}

func main() {
	var mdFiles []string
	root := "../ml-engineering"

	err := filepath.Walk(root, visit(&mdFiles))
	if err != nil {
		panic(err)
	}

	for _, filePath := range mdFiles {
		fmt.Println("开始处理文件", filePath)

		buf, err := os.ReadFile(filePath)
		if err != nil {
			fmt.Printf("读取文件出错: %s\n", err)
			return
		}

		translated, err := query(string(buf))
		if err != nil {
			fmt.Printf("调用模型翻译出错: %s\n", err)
			return
		}

		err = os.WriteFile(filePath, []byte(translated), 0644)
		if err != nil {
			fmt.Printf("保存文件出错: %s\n", err)
			return
		}
		fmt.Println("搞定!")
	}
}

将上面的代码保存为 main.go,然后再次执行 go run main.go,我们将得到类似下面的结果:

# go run main.go
开始处理文件 ../ml-engineering/README.md
搞定!
开始处理文件 ../ml-engineering/build/README.md
搞定!
开始处理文件 ../ml-engineering/compute/README.md
搞定!
开始处理文件 ../ml-engineering/compute/accelerator/README.md
搞定!
开始处理文件 ../ml-engineering/compute/accelerator/nvidia/debug.md
...

耐心等待程序请求结束,每一篇内容大概需要花费十几秒左右的时间,来等待模型处理。如果你在看本篇文章的时候已经准备好了饮料和书,那么现在就是一边喝饮料,一边看书的时刻啦。

被模型自动翻译后的内容

稍等片刻,当模型处理完所有文件后,我们就能够得到模型翻译电子书的第一版中文内容啦。

当然,这里只是翻译的第一步,类似训练模型分为不同的阶段,这个阶段,我们使用模型和一杯饮料,就可以快速的完成整本书的低成本的快速翻译。

蛮好喝的,推荐~

至于精细化翻译,以及名词一致性调整,额外的内容的总结和摘要,或者风格化口语翻译,我们后面慢慢来。

最后

本篇文章,我们就先聊到这里,下一篇相关的文章里,我们来聊聊更精细的翻译细节处理,以及模型应用的复杂流水线的编排。

–EOF