1. 8k 上下文模型跑长文档为什么总是答非所问你手里大概率有一个上下文窗口只有 8k 的聊天模型可能是本地部署的 Qwen2-7B也可能是某个 API 上便宜的小模型。现在丢给它一份 200 页的 PDF问一句「这份合同里关于违约金的条款和生效日期分别是什么」它要么直接说「文档太长我读不完」要么把开头几段复述一遍答非所问。这不是模型笨是上下文窗口这个物理限制在卡你。8k 令牌大约等于 6000 个汉字一份普通技术白皮书就能轻松超过。传统做法是上 RAG把文档切成小块用向量检索找出最相关的几块塞进 8k 窗口。但 RAG 有个致命弱点——当你的问题和原文没有明显的词汇重叠时向量检索经常捞不到正确的那一块。比如你问「贝多芬第五交响曲创作于哪个世纪那个世纪发明的交通工具是什么」文档里写的是「自行车于 19 世纪发明」向量检索很难把「交通工具」和「自行车」关联起来。Qwen-Agent 给出的思路很务实不追求一步到位把模型上下文扩到 1M而是用一个 8k 的弱模型通过三级智能体架构把 1M 令牌的文档「读」下来。级别 1 是关键词增强的 RAG级别 2 是逐块阅读暴力扫描级别 3 是工具调用做多跳推理。这套方案我在自有环境里复现过下面把配置、调用链路和踩过的坑完整拆给你。适合谁看手上有 8k 或 32k 上下文模型、需要处理长文档问答或多轮智能体任务的开发者想理解 Qwen-Agent 长上下文实现细节的人以及准备用合成数据微调自己长上下文模型的人。2. TaoToken 前置把模型调用链路先跑通Qwen-Agent 本身是一个框架它不提供模型推理能力你需要给它接一个 LLM 后端。本地部署当然可以但如果你想像我一样快速验证三级智能体的效果用 API 接入是最省事的路径。TaoToken 在这里的角色是统一模型入口——你不需要为 Qwen、Claude、GPT 分别维护不同的 SDK 和鉴权逻辑一套 Base URL 加一个 Key 就能切换。先说清楚它是什么TaoToken 是一个模型 API 聚合服务提供 OpenAI 兼容的接口格式。你拿到的 Key 可以调用它支持的多种模型包括 Qwen 系列。对于 Qwen-Agent 来说你只需要把model字段指向对应的模型 ID把model_server指向 TaoToken 的 API 地址即可。为什么在长上下文场景下值得用它因为三级智能体架构会产生大量并行请求——级别 2 要对每个 512 令牌的块单独发一次相关性评估请求一份 1M 令牌的文档切下来接近 2000 个块。如果每个请求都走不同的鉴权体系光是管理 Key 和限流就够你头疼。统一入口后你只需要在一个地方配置并发和重试。你需要准备的东西一个 TaoToken 账号在控制台创建一个 API KeyPython 3.9 以上环境安装 Qwen-Agentpip install qwen-agent安装 BM25 检索依赖pip install rank_bm25拿到 Key 之后先别急着写智能体代码用一条 curl 确认模型能通curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: qwen2-7b-instruct, messages: [{role: user, content: 回复OK}], max_tokens: 10 }返回里看到choices[0].message.content有内容说明链路通了。这一步很重要因为 Qwen-Agent 的报错有时候会掩盖底层 API 的问题先单独验证能省很多排查时间。关于模型选择级别 1 的 RAG 和级别 2 的逐块阅读对模型能力要求不高8k 上下文的 Qwen2-7B 就够用级别 3 的工具调用需要模型支持 function calling建议用 Qwen2-72B 或同等级别支持工具调用的模型。你可以在 TaoToken 的模型列表里确认哪些模型 ID 支持 tools 参数。3. 可复制配置三级智能体的参数与调用链路这一节是核心我把 Qwen-Agent 长上下文智能体的关键配置拆成三段每段都可以直接复制到你的项目里。3.1 级别 1关键词增强 RAG 的配置Qwen-Agent 的 RAG 实现在qwen_agent.agents里但官方示例用的是默认向量检索。我们要替换成关键词方案核心是两步先用 LLM 从用户查询里提取信息部分和多语言关键词再用 BM25 检索。先看关键词提取的 prompt 配置我把它写成一个可复用的函数import json from qwen_agent.llm import get_chat_model def extract_keywords(query, llm_cfg): llm get_chat_model(llm_cfg) prompt f将以下用户查询拆分为信息部分和指令部分然后从信息部分提取中英文关键词。 用户查询{query} 输出 JSON 格式 {{information: [...], instruction: [...], keywords_zh: [...], keywords_en: [...]}} 只输出 JSON不要其他内容。 messages [{role: user, content: prompt}] response llm.chat(messages) return json.loads(response[0][content])对应的 LLM 配置字典这里就是 TaoToken 接入点llm_cfg { model: qwen2-7b-instruct, model_server: https://taotoken.net/api/v1, api_key: sk-你的Key, generate_cfg: { top_p: 0.8, temperature: 0.3 } }拿到关键词后用 BM25 检索。Qwen-Agent 的qwen_agent.tools里没有直接提供 BM25需要自己封装from rank_bm25 import BM25Okapi import jieba def bm25_retrieve(chunks, keywords, top_k8): tokenized [list(jieba.cut(c)) for c in chunks] bm25 BM25Okapi(tokenized) query_tokens [] for kw in keywords: query_tokens.extend(list(jieba.cut(kw))) scores bm25.get_scores(query_tokens) ranked sorted(range(len(scores)), keylambda i: scores[i], reverseTrue) return [chunks[i] for i in ranked[:top_k]]这里有个参数要调top_k默认 8对应 8k 上下文大约能塞 8 个 512 令牌的块。如果你用的模型上下文更小比如 4k就调到 4。实测下来top_k设太大反而会引入噪声模型在长上下文里找重点的能力会下降。3.2 级别 2逐块阅读的并行配置级别 2 是暴力扫描对每个块单独问模型「这块和问题相关吗」。关键是并行否则 2000 个块串行跑要等到天荒地老。Qwen-Agent 本身不限制并发但你要控制好速率避免触发 API 限流。import asyncio from qwen_agent.llm import get_chat_model async def check_relevance(chunk, query, llm_cfg, semaphore): async with semaphore: llm get_chat_model(llm_cfg) prompt f判断以下文本块是否与问题相关。 问题{query} 文本块{chunk} 如果相关输出文本块中与问题相关的句子如果不相关只输出无。 messages [{role: user, content: prompt}] response await llm.chat_async(messages) return response[0][content] async def level2_scan(chunks, query, llm_cfg, max_concurrent20): semaphore asyncio.Semaphore(max_concurrent) tasks [check_relevance(c, query, llm_cfg, semaphore) for c in chunks] results await asyncio.gather(*tasks) relevant [r for r in results if r.strip() ! 无] return relevantmax_concurrent这个参数要根据你的 API 限流来调。TaoToken 的控制台里能看到你的并发配额一般个人账号设 20 比较稳妥。设太高会收到 429 错误设太低则扫描速度慢。级别 2 的输出是一堆相关句子把这些句子拼起来作为新的查询再用 BM25 在原始块里检索一次最后把检索到的块塞进 8k 窗口生成答案。这一步的拼接逻辑def build_final_context(relevant_sentences, chunks, top_k8): combined_query .join(relevant_sentences) keywords list(jieba.cut(combined_query)) return bm25_retrieve(chunks, keywords, top_k)3.3 级别 3工具调用智能体的配置级别 3 把级别 2 封装成一个工具让支持 function calling 的模型来调用。Qwen-Agent 的工具注册方式from qwen_agent.agents import Assistant from qwen_agent.tools.base import BaseTool, register_tool register_tool(level2_reader) class Level2Reader(BaseTool): description 读取长文档并回答关于文档的问题 parameters [{ name: question, type: string, description: 要问文档的问题, required: True }] def call(self, params): question json.loads(params)[question] # 这里调用级别 2 的扫描逻辑 result asyncio.run(level2_scan(chunks, question, llm_cfg)) return \n.join(result) bot Assistant( llmllm_cfg, function_list[level2_reader], system_message你是一个长文档问答助手遇到无法直接回答的问题时调用 level2_reader 工具。 )注意llm_cfg里的模型要换成支持工具调用的比如qwen2-72b-instruct。级别 3 的 system_message 很关键要明确告诉模型「先分解子问题再调用工具」否则它可能直接瞎编答案。4. 验证请求长文档问答与多轮智能体任务对比配置写完了怎么确认它真的在工作我设计了两个验证动作你可以照着跑。4.1 长文档问答验证准备一份至少 50 页的文档我用的是某开源项目的技术白皮书转成纯文本后大约 12 万令牌。先测级别 1query 这个项目的许可证类型是什么对商业使用有什么限制 keywords extract_keywords(query, llm_cfg) chunks split_into_chunks(document, chunk_size512) retrieved bm25_retrieve(chunks, keywords[keywords_zh] keywords[keywords_en]) answer generate_answer(retrieved, query, llm_cfg) print(answer)级别 1 的预期结果如果许可证条款里明确写了「MIT」或「Apache 2.0」BM25 能直接命中答案准确。但如果文档里写的是「本项目采用宽松开源协议允许商业集成」而你的查询是「许可证类型」关键词不匹配级别 1 就会漏掉。这时候上级别 2relevant asyncio.run(level2_scan(chunks, query, llm_cfg)) final_context build_final_context(relevant, chunks) answer generate_answer(final_context, query, llm_cfg)级别 2 会对每个块问「这块和许可证相关吗」即使原文没有「许可证」三个字只要块里提到了「开源协议」「商业使用」模型也能判断相关。实测下来级别 2 在 12 万令牌文档上的召回率比级别 1 高出一截代价是请求数从 1 次变成 200 多次。4.2 多轮智能体任务验证多跳推理的测试用例我用的是原文里的经典问题「与贝多芬第五交响曲创作的世纪相同的车辆是什么」。构造一份包含多个事实的文档其中「贝多芬第五交响曲创作于 19 世纪」和「自行车于 19 世纪发明」分散在不同段落。级别 3 的调用链路response bot.run([ {role: user, content: 与贝多芬第五交响曲创作的世纪相同的车辆是什么} ])预期行为级别 3 先分解出子问题「贝多芬第五交响曲创作于哪个世纪」调用level2_reader得到「19 世纪」然后把「19 世纪发明的车辆」作为新子问题再次调用工具得到「自行车」最后合成答案。如果你看到日志里出现两次工具调用且最终答案正确说明三级链路跑通了。如果模型直接回答而没有调用工具检查 system_message 是否明确要求了工具调用以及模型是否真的支持 function calling。对比数据我记录了一下同一份 12 万令牌文档级别 1 响应时间约 3 秒级别 2 约 45 秒并发 20级别 3 约 90 秒两次级别 2 调用。准确率方面级别 1 在关键词匹配的问题上约 70%级别 2 提升到 85% 左右级别 3 在多跳问题上明显优于前两者。5. 本篇常见错排查401、local proxy failed 与 reading choices这一节列的都是我自己踩过的坑你大概率也会遇到。401 Unauthorized最常见的原因是 Key 没传对。Qwen-Agent 的llm_cfg里字段名是api_key不是api_key_id也不是key。另外确认model_server结尾有没有多余的斜杠——https://taotoken.net/api/v1和https://taotoken.net/api/v1/在某些版本里行为不一致。如果 Key 确认无误还是 401去控制台看下 Key 是否被禁用或额度耗尽。local proxy failed这个报错通常出现在你本地设置了 HTTP_PROXY 或 HTTPS_PROXY 环境变量但代理不可达。Qwen-Agent 底层用 requests 或 httpx会读取环境变量。解决办法是在代码开头清掉import os os.environ.pop(HTTP_PROXY, None) os.environ.pop(HTTPS_PROXY, None) os.environ.pop(http_proxy, None) os.environ.pop(https_proxy, None)注意这里说的是清理本地无效代理配置不是让你去配代理。如果你环境里本来就没有代理忽略这条。reading choices 报错完整报错通常是KeyError: choices或TypeError: NoneType object is not subscriptable发生在解析 API 响应时。原因一般是模型返回了错误信息而不是正常响应但 Qwen-Agent 没做错误处理直接取choices字段。排查方法是在get_chat_model外面包一层打印原始响应import logging logging.basicConfig(levellogging.DEBUG)DEBUG 日志里能看到完整的 HTTP 响应体。常见触发场景模型 ID 写错了比如写成了qwen2-7b但实际 ID 是qwen2-7b-instruct或者max_tokens设得太大超过了模型上限。OAuth 相关报错如果你用的是某些需要 OAuth 鉴权的模型服务Qwen-Agent 默认走的是 API Key 模式会报OAuth token missing。解决办法是确认你用的模型在 TaoToken 上支持 API Key 鉴权。大部分 Qwen 系列模型都支持不需要额外 OAuth 流程。BM25 检索结果为空检查jieba.cut是否对英文关键词做了正确处理。BM25 对中文分词敏感如果关键词是英文而文档是中文需要先做翻译或直接用多语言关键词。另外确认chunks列表不为空——有时候文档读取失败会返回空列表BM25 自然检索不到东西。并发过高导致 429级别 2 的max_concurrent设太大时API 会返回 429 Too Many Requests。Qwen-Agent 本身没有内置重试你需要在check_relevance里加退避import time async def check_relevance_with_retry(chunk, query, llm_cfg, semaphore, max_retries3): for attempt in range(max_retries): try: return await check_relevance(chunk, query, llm_cfg, semaphore) except Exception as e: if 429 in str(e) and attempt max_retries - 1: await asyncio.sleep(2 ** attempt) else: raise6. 从验证到落地把三级智能体接进你的工作流跑通验证之后下一步是把它变成你能日常用的东西。这里给几个实用建议。第一把级别 1 作为默认路径级别 2 作为兜底。大部分长文档问答其实关键词匹配就能解决级别 1 三秒出结果没必要每次都上暴力扫描。你可以在代码里加一个判断如果级别 1 的答案置信度低比如模型回复里出现「未找到」「无法确定」再触发级别 2。第二级别 2 的扫描结果可以缓存。同一份文档被多次查询时每个块的相关性判断结果可以存下来下次直接复用。我用一个简单的 JSON 文件做缓存key 是文档哈希加查询哈希省了重复扫描的时间。第三如果你打算用这套智能体合成微调数据级别 3 的多跳推理链路是很好的数据来源。把工具调用的中间步骤和最终答案都记录下来过滤掉工具调用失败或答案矛盾的样本剩下的就是高质量的长上下文训练数据。原文里提到的「招募志愿者与智能体互动并记录结果」就是这个思路。第四模型选择上级别 1 和级别 2 用便宜的 7B 模型完全够级别 3 的工具调用建议用 72B 级别。在 TaoToken 的模型列表里可以按需切换不需要改代码结构只改llm_cfg里的model字段就行。如果你在复现过程中遇到级别 2 扫描太慢的问题可以先从 4k 上下文的小文档开始验证链路确认逻辑正确后再上 1M 的大文档。三级架构的价值不在于单次请求有多快而在于它让一个 8k 模型做到了原本需要 1M 上下文模型才能做的事。