项目背景与目标
前面的文章分别验证了分词、文本表示、Transformer、生成参数、Prompt、模型 API 和本地推理。本项目把这些能力收束成一个批量文本任务工作台:从 JSONL 读取自创运行消息,根据任务生成摘要或客观改写,并把结果、耗时和 Token 用量写回文件。
项目支持三个后端:
demo:纯标准库的离线后端,用于验证数据流和错误处理,不代表模型质量;api:调用 Chat Completions 兼容接口;local:使用 Transformers 加载本地或已缓存的指令模型。
阶段边界保持明确:不加入 RAG、结构化输出、Function Calling、工具调用和 Agent 编排。项目数据、编号、任务和名称均为自创。

三种后端共享消息构造和输出记录。这样可以在不改变上层批处理流程的情况下比较离线链路、在线 API 与本地模型。
整体架构
JSONL 输入
↓
字段校验 ──失败──> 错误记录
↓
Prompt Builder
↓
Backend Protocol
├── DemoBackend
├── ApiBackend
└── LocalBackend
↓
结果、Token、耗时、错误
↓
JSONL 输出 + 汇总
项目采用单文件实现,便于先跑通完整流程:
examples/06.15-text-workbench/
├── text_workbench.py
├── sample_input.jsonl # 首次运行自动创建
└── output.jsonl # 每次批处理生成
完整代码位于 text_workbench.py。
核心流程
输入协议
每行是一个独立 JSON 对象:
{"id":"m-001","task":"summary","text":"节点 NX-8 网络超时;11:05 切换线路后恢复。"}
task 只允许:
summary:生成不超过 60 字的事实摘要;rewrite:改写为简洁、客观的运行记录。
任务白名单由程序控制,不把任意任务名称直接交给模型。输入为空、JSON 损坏或任务不支持时,当前记录写入错误,其他记录继续处理。
Prompt 构造
build_messages() 把任务规则与外部输入分开:
def build_messages(task: str, text: str) -> list[dict[str, str]]:
instructions = {
"summary": "任务:生成一句中文摘要,不超过60字。保留编号、数值和当前状态,不补充原文外事实。",
"rewrite": "任务:改写为简洁、客观的运行记录。保留编号、数值和当前状态,不增加处理建议。",
}
if task not in instructions:
raise ValueError(f"unsupported task: {task}")
return [
{"role": "system", "content": "只处理 <text> 中的内容;信息不足时明确说明,不猜测原因。"},
{"role": "user", "content": f"{instructions[task]}\n<text>\n{text.strip()}\n</text>"},
]
<text> 只是输入分隔符,不是安全机制。任务白名单、输入长度、文件权限和结果检查仍由程序负责。
统一后端接口
三个后端都返回同一种结果:
@dataclass
class GenerationResult:
text: str
input_tokens: int | None = None
output_tokens: int | None = None
class Backend(Protocol):
name: str
def generate(self, messages: list[dict[str, str]]) -> GenerationResult:
...
统一接口让批处理逻辑不需要知道请求走网络还是显卡。Token 数据缺失时保留 None,而不是伪装成精确值。
关键实现
单条失败不终止整批任务
process_file() 按行捕获异常,把错误类型写入当前输出记录。这样既能保留成功结果,也能回查具体失败行。输出不记录 API Key,原文使用短哈希建立对应关系,降低普通运行日志复制敏感文本的风险。
Demo 后端用于验证流程
Demo 后端只做确定性的截断和替换,可以离线测试:
- JSONL 读取与写入;
- Task 白名单;
- Prompt 组装;
- 成功与失败计数;
- 耗时和 Token 字段;
- 输出路径与字符编码。
它不能用于评价摘要质量,也不能证明在线或本地模型效果。
API 与本地模型的差异被限制在后端内部
API 后端读取 MODEL_API_BASE、MODEL_API_KEY 和 MODEL_NAME;本地后端读取 LOCAL_MODEL_ID,并使用对应 Tokenizer 的 Chat Template。上层代码只调用 backend.generate(messages)。
运行项目
进入项目目录:
cd "examples/06.15-text-workbench"
1. 离线跑通
python text_workbench.py --init-data --backend demo
汇总输出类似:
{
"backend": "demo",
"total": 3,
"succeeded": 3,
"failed": 0,
"total_seconds": 0.001,
"output": ".../output.jsonl"
}
2. 切换在线 API
python -m pip install httpx
export MODEL_API_BASE="https://provider.example/v1"
export MODEL_API_KEY="replace-with-your-key"
export MODEL_NAME="provider-model-name"
python text_workbench.py --backend api --output output-api.jsonl
接口字段必须按实际服务文档核对,不应只因为服务宣称“兼容”就假设全部响应细节相同。
3. 切换本地模型
python -m pip install "transformers" "torch" "accelerate"
export LOCAL_MODEL_ID="Qwen/Qwen2.5-0.5B-Instruct"
python text_workbench.py --backend local --output output-local.jsonl
第一次运行需要下载模型。离线环境可把 LOCAL_MODEL_ID 指向提前准备的本地目录。
结果、评估与阶段复盘
输出记录包含:
{
"id": "m-001",
"task": "summary",
"backend": "demo",
"input_sha256": "...",
"result": "节点 NX-8 网络超时;11:05 切换线路后恢复",
"input_tokens": 51,
"output_tokens": 21,
"elapsed_seconds": 0.0,
"error": null
}
比较 API 和本地后端时,至少统一:
- 输入文件和 Prompt 版本;
- 是否使用采样以及生成长度;
- 任务通过条件;
- 冷启动与预热后的耗时口径;
- Token 统计来源;
- 失败、超时和空输出的处理方式。
项目的结果检查可以沿用 06.12 的固定样本集,统计编号、数值、状态是否保留,是否添加原文外事实,以及输出长度是否合规。对开放摘要还应人工抽查事实一致性,不能只比较字符串相似度。
这一阶段最大的变化不是多调用了一个模型,而是模型被放进了可替换、可记录、可失败的程序边界中。Prompt、模型和后端都可能变化,输入协议、错误记录和比较方法应保持稳定。
遇到的问题
Demo 正常,API 全部失败
优先检查环境变量、Endpoint、认证头、模型名称和服务端响应字段。打印状态码与请求标识,不要打印完整密钥和敏感正文。
本地模型生成结果为空
检查 Chat Template、输入长度、切片起点和停止 Token。解码时只应截取输入长度之后的新 Token。
两个后端 Token 数无法直接比较
不同模型使用不同 Tokenizer,同一文本的 Token 数可能不同。Token 用量适合计算各自成本和吞吐,不应被当作跨模型的文本长度绝对标准。
输出文件中找不到原文
这是有意的日志最小化设计。需要人工复核时,应通过受控数据源使用 id 关联原文,而不是在所有运行日志中复制完整输入。
改进方向
- 增加输入长度上限和并发控制;
- 为可重试错误加入有限退避;
- 保存 Prompt 版本、模型 revision 和依赖版本;
- 增加基于固定样本集的自动检查报告;
- 将后端配置移入独立配置文件并加入密钥管理;
- 下一阶段再引入结构化输出、文档检索与工具调用。
小结
文本任务工作台把 NLP 与大语言模型阶段串成了完整链路:文本进入统一协议,Prompt 负责表达任务,后端负责生成,程序记录结果、用量、耗时和错误。后端可切换后,在线 API 与本地模型才具备可比较、可维护的基础。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


