登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

Hugging Face Transformers 怎样只输出生成结果而不带输入提示词

来源:17golang原创

时间:2026-11-07 23:30:52 145浏览 收藏

在 Hugging Face Transformers 中调用 model.generate() 后,如果直接把返回的整行 token 交给 tokenizer 解码,decoder-only 模型通常会把输入提示词一起返回。这不是模型重复生成,而是返回序列包含了“输入 token + 新生成 token”。真正需要做的是先确定输入长度,再只解码新增部分。

官方文档:https://huggingface.co/docs/transformers/

这篇只讨论生成结果的边界,不涉及模型训练。最小原则可以先记住:decoder-only 按有效输入长度切片;encoder-decoder 不要机械套用同一个切片公式;批量输入要按行处理 padding。

问题现场:为什么输出里重复了 prompt

常见代码大致是这样:

from transformers import AutoTokenizer, AutoModelForCausalLM

model_name = "distilgpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

prompt = "Explain why caching helps inference."
inputs = tokenizer(prompt, return_tensors="pt")

# max_new_tokens 只限制新增 token,不代表返回值只含新增 token。
output_ids = model.generate(**inputs, max_new_tokens=32)

# 直接解码整行会把 prompt 也带回结果。
full_text = tokenizer.decode(output_ids[0], skip_special_tokens=True)
print(full_text)

这里的 output_ids[0] 是完整序列。对 decoder-only 模型,它通常从输入 token 开始,后面才是模型新生成的 token。因此修复重点不是把 prompt 从字符串里做替换,而是在 token 层找到边界。

tokenizer、input_ids、generate 和完整 sequences 之间的输入与新生成 token 静态关系图
图1:生成序列边界说明图。左侧是输入 token,中间是 generate 返回的完整序列,右侧标出只应解码的新增 token;这是静态结构示意,不是运行截图。

最小修复:按输入长度截出新生成 token

单条、无 padding 的 decoder-only 输入,可以使用输入序列长度作为切片起点:

# input_ids 的长度就是完整序列中“输入提示词”占用的 token 数。
input_length = inputs["input_ids"].shape[1]
new_token_ids = output_ids[0, input_length:]

# 只解码切片后的 token,得到新增文本。
new_text = tokenizer.decode(new_token_ids, skip_special_tokens=True)
print(new_text)

这段代码的关键不是 decode 的参数,而是先在 token 序列上切片。字符串层面用 replace(prompt, "") 有两个问题:模型可能改变空格或换行,prompt 也可能在生成内容中自然出现,字符串替换无法表达 token 边界。

max_new_tokens 仍然值得保留,它控制最多生成多少个新 token;它不会改变返回序列是否包含输入,也不是“只返回新文本”的开关。

从单条输入扩展到批量输入

批量场景里,不能把第一行的长度套给所有样本。右 padding 与左 padding 还会让输入张量的形状和有效 token 位置不同,所以应先确定 tokenizer 的 padding 策略,再按每一行的有效长度切片。

prompts = [
    "Explain cache locality in one paragraph.",
    "Explain tokenizer padding in one paragraph.",
]

# padding=True 让 batch 形成矩形;attention_mask 标出每行有效输入位置。
batch = tokenizer(prompts, padding=True, return_tensors="pt")
batch_output = model.generate(**batch, max_new_tokens=32)

texts = []
for row_ids, row_mask in zip(batch_output, batch["attention_mask"]):
    # 只统计有效输入 token,避免把 padding 长度当成 prompt 长度。
    valid_input_length = int(row_mask.sum().item())
    new_ids = row_ids[valid_input_length:]
    texts.append(tokenizer.decode(new_ids, skip_special_tokens=True))

for text in texts:
    print(text)

上面的写法适合右 padding 的常见批量输入。若模型和 tokenizer 使用左 padding,完整输出序列的前缀布局需要结合模型的 padding 配置确认,不能只凭“总长度减去 padding 数量”猜切片位置。这里的检查点是:每行的有效输入长度、模型返回序列形状、解码后的新文本三者要对应。

decoder-only 和 encoder-decoder 不能混用公式

GPT 类 decoder-only 模型把输入作为生成序列的前缀,因此按输入长度截取通常成立。T5、BART 这类 encoder-decoder 模型的输入先进入 encoder,decoder 生成自己的序列,返回序列一般不等于“encoder 输入 token 加上 decoder 新 token”。

因此写通用封装时,先根据模型架构决定规则:

模型形态返回序列的关注点处理建议
decoder-only输入 token 常位于返回序列前缀按每行有效输入长度切片后 decode
encoder-decoderdecoder 输出有自己的序列边界直接解码生成序列,并按特殊 token 规则处理
批量输入padding 会改变张量形状读取 attention_mask,逐行处理
批量 input_ids、padding、attention_mask 与两种模型输出边界的静态关系图
图2:批量解码边界说明图。上层对比 decoder-only 与 encoder-decoder 的序列关系,下层展示 batch、padding、attention_mask 和逐行切片的对应关系;这是静态结构示意,不是运行截图。

什么时候用 return_dict_in_generate

如果只要文本,默认返回的序列就够用;如果还要分数、hidden states、attentions 或 cache,可以打开结构化返回:

# 结构化返回用于同时读取 sequences 和可选的 scores 等字段。
generation = model.generate(
    **inputs,
    max_new_tokens=32,
    return_dict_in_generate=True,
    output_scores=True,
)

# sequences 仍然需要按模型类型判断边界,不会自动替你删除 prompt。
sequence_ids = generation.sequences[0]
input_length = inputs["input_ids"].shape[1]
new_text = tokenizer.decode(sequence_ids[input_length:], skip_special_tokens=True)

return_dict_in_generate=True 解决的是“返回哪些结构化字段”,不是“返回值是否只含新文本”。把这两个问题分开,排查会简单很多。

Pipeline 返回值为什么看起来不一样

如果改用 pipeline("text-generation"),pipeline 可能根据参数把完整文本或只包含新增文本的结果整理成字典。这里要以当前 Transformers 版本对应的 pipeline 参数和返回结构为准,不要把 pipeline 的行为直接推断到底层 model.generate()。

from transformers import pipeline

generator = pipeline("text-generation", model="distilgpt2", tokenizer="distilgpt2")

# return_full_text 是 pipeline 层的结果整理选项;它不是 generate 的参数。
result = generator(
    "Explain cache locality.",
    max_new_tokens=32,
    return_full_text=False,
)
print(result[0]["generated_text"])

如果你的项目已经封装了 pipeline,可以优先使用它提供的结果选项;如果项目直接调用模型,则保留 token 级切片逻辑。两层 API 不要混着猜。

一张表排查“仍然带 prompt”

现象优先检查处理方向
输出从输入句子开始是否直接 decode 完整 sequencesdecoder-only 按有效输入长度切片
批量结果互相错位是否复用了第一行长度按 attention_mask 逐行计算
打开结构化返回后仍带输入是否误把 return_dict_in_generate 当裁剪开关仍然处理 sequences 的边界
pipeline 与底层结果不同是否混用了 pipeline 与 generate 的参数分别按对应 API 处理

结论

“只输出生成结果”的稳定做法是先辨认模型架构,再在 token 层处理边界。对 decoder-only,单条输入通常从 input_ids.shape[1] 开始切;对批量输入,要结合每行的 attention_mask;对 encoder-decoder,不要把 encoder 输入当成 decoder 输出前缀。最后再调用 tokenizer 解码,得到的文本才不会因为字符串替换而留下边界问题。

相关问题

max_length 和 max_new_tokens 有什么区别? max_new_tokens只计算新增 token,更适合限制生成长度;max_length描述完整序列长度,旧代码里更容易和输入长度混淆。

为什么生成结果有时多一个空格? token 合并和 tokenizer 的空格规则可能让解码后的文本出现自然的前后空白。应先完成 token 切片,再按业务需要处理首尾空白。

能不能用字符串 replace 删除 prompt? 不建议。prompt 可能在答案中再次出现,字符串替换也无法可靠处理 token 化后的空格、换行和特殊 token。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>