fix(core): cite web search sources as bare URLs - #11677
Conversation
The citation policy and tool description asked for markdown links, but the search result lists pages by URL only, so the model wrote link text itself: URL paths or plausible titles for pages it never saw. Ask for bare URLs, one per line, without titles or link text, and say why. Cite-order rules are unchanged. The title-extraction alternative (QwenLM#11616) is withdrawn; see QwenLM#11564.
The search side model's narrated findings can carry its own link text, so "page titles are not part of this result" is not always true. State the accurate reason: the page lists give URLs only, so a title in the answer, even one repeated from the narration, cannot be verified.
|
Thanks for the PR — and for writing up the withdrawal of #11616 rather than just quietly closing it. That comment is what makes this one reviewable. Template looks good ✓ Problem: observed, not theoretical, and I confirmed it statically against Direction: aligned. This is decision 2's third option, the one #11564 itself enumerates ("or no titles at all with the citation policy asking for bare URLs"), and the prior triage on that issue argued it was the only zero-parser-surface choice. I re-verified the two rendering claims that make the tradeoff survivable, since everything rests on them: One question, raised as a question and not a block: #11564 still carries Size: core paths touched ( Approach: nothing to cut. I went looking for a third site that still says "markdown links" and there isn't one — repo-wide, the string appears exactly twice and you changed both, and the only Risk: no elevated risk signals — none of the changed files match the revert-correlated path list. The one thing I checked that could actually have broken something is the result envelope: the policy grows ~156 chars and is appended after the body is bounded to Moving on to code review. 🔍 中文说明感谢贡献——也感谢你把 #11616 的撤回过程写清楚,而不是直接关掉。正是那段说明让这个 PR 变得可评审。 模板完整 ✓ 问题: 是已观测到的问题,不是理论性加固;我没有只信 PR 描述,而是对 方向: 对齐。这正是 #11564 自己列出的第 2 项决策的第三个选项("干脆不要标题、让引用策略要求裸 URL"),该 issue 上一次分诊也论证过这是唯一解析面为零的选择。这个取舍能否成立完全取决于两条渲染事实,我重新核对过: 有一个问题,作为提问而非阻断提出:#11564 仍带 规模: 触及核心路径( 方案: 无可裁剪。我专门去找是否还有第三处写着 "markdown links",没有——全仓库该字符串只出现两次,你都改了;文件里唯一的 风险: 无升级风险信号——改动文件均不匹配与 revert 相关的路径清单。我唯一认真查过、且真可能出问题的地方是结果信封:策略变长约 156 字符,而它是在 body 被限制到 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewSmall diff, so I read the whole surface around it rather than just the three changed lines. No Criticals and no AGENTS.md violations. What follows is what I verified, because for a prompt-wording change the interesting question isn't "is the string well-formed" but "did you get all of it, and does anything downstream assume the old shape". The change is complete. Your two negative assertions are non-vacuous. This is worth calling out because The new The policy's factual premise is true, not aspirational. "The page lists above give URLs only" is the load-bearing justification for refusing titles, so I checked it rather than trusting it: both evidence tiers are built by mapping each source to a bare The result envelope still fits. This is the one way a longer footer could have broken something real. Two non-blocking observations, neither needing action:
Test evidence — the PR's own CIUnattended CI run, so under the static-review rule I built nothing and executed nothing: no No failing job, so there is no log excerpt to quote. The macOS and Windows Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified: your 105-passing unit run, the 3/3 revert-catch claim, the clean Sandboxed verification would settle this: Real-scenario testing (2c) is not applicable here: this is an unattended CI run, which never drives tmux. The live-behaviour gap is the 中文说明代码评审改动很小,所以我读的是它周围的整片面,而不只是那三行。没有 Critical,也没有违反 AGENTS.md。下面写的是我实际核对过的内容——对一处提示词文案改动来说,有意思的问题不是"字符串写得对不对",而是"你改全了没有,以及下游有没有东西依赖旧形状"。 改动是完整的。 你的两条否定断言不是空断言。 值得专门点出来,因为 新增 策略里的事实前提为真,不是一厢情愿。 "the page lists above give URLs only" 是拒绝标题的关键论据,所以我核对了它而不是直接相信:两个证据层级都是把每个来源映射为一条裸 结果信封仍然放得下。 这是"更长的尾注"唯一可能真正弄坏东西的途径。 两条不阻断的观察,均无需处理:
测试证据 —— 本 PR 自己的 CI这是无人值守的 CI 运行,因此按静态评审规则我没有构建、也没有执行任何东西:没有 没有失败的作业,因此没有日志摘录可引。macOS 与 Windows 的 未验证: 你说的 105 个单测通过、3/3 的回退捕获、 沙箱化验证可以定这件事: 真实场景测试(2c)在此不适用:这是无人值守的 CI 运行,从不驱动 tmux。上述 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 3/5 — the diff itself I'd merge without hesitation; what I can't settle from the diff is whether this product decision was ours to make yet. Before reading the diff I asked what I'd do about a model inventing page titles when the pipeline hands it only URLs. There are two shapes of answer: supply real titles, or stop asking for them. The first was attempted twice in this repo — #11490 went 16 findings to 29, #11616 went 16 to 29 plus a human-corroborated Critical, and in both cases most new findings were induced by the previous round's fixes. That's the signature of a mechanism whose surface grows faster than it can be secured. The second shape is three lines. This PR is the second shape, so it matches my independent proposal and lands on the convergent branch of a fork that had already diverged twice. I did not find a simpler path it missed. The thing I'd want to say out loud, because it's easy to read a chain of four related PRs as volume and miss that this one is different in kind: the earlier three added machinery — a list parser, a title cleaner, URL normalization, a destination guard. This one deletes a requirement. It doesn't secure the title channel, it removes it, and the review divergence on #11490 and #11616 is evidence for it rather than against it. Judged on its own merits it's the right call, and I don't think I'm being worn down into accepting it. It also solves something users actually feel. A fabricated title in a citation isn't cosmetic: the user reads a page name that doesn't exist and attributes it to the publisher, and the URL underneath is the only honest part. That was happening on a surface #11348 turned on by default. If I were maintaining this in six months I'd thank whoever wrote it, for a reason that isn't the fix itself. The new wording carries its own justification — the page lists above give URLs only, so a title cannot be verified — which means the next person tempted to re-add titles has to argue with a stated reason instead of silently reverting a string. That's the difference between a wording change and a decision that survives. So the code is not what I'm deferring on. Two things are, and I want to be precise about both because neither is resolvable from the diff, the tests, or the PR body: 1. The 2. The behavioural claim is the author's, and I can't close the gap from here. Everything about whether models actually comply — 9 of 9 bare URLs across three After runs, 11 of 11 on the first wording revision, no markdown links — is author-reported, Linux only, one model family. The new unit tests pin the wording, which is the right thing for them to pin and I confirmed both negative assertions genuinely bite, but a prompt-wording change is exactly the case where a green suite proves the strings changed and nothing about whether the behaviour did. On this path I execute no PR code, so Neither of those is a defect in the PR. Escalating rather than approving, because the gate's job here is to get a maintainer to own the product decision, and approving would let it merge on my read of an argument a maintainer never weighed in on. @yiliang114 — you're already assigned, which is the right routing by the owner map's path rules for 中文说明Confidence: 3/5 —— 这份 diff 本身我会毫不犹豫合并;我无法从 diff 判断的是,这个产品决定是否已经轮到我们拍板。 在读 diff 之前我先问自己:模型在只拿到 URL 的情况下编造页面标题,我会怎么处理。答案只有两种形状:提供真实标题,或者不再索要标题。第一种在本仓库已经试过两次——#11490 从 16 条意见涨到 29 条,#11616 从 16 条涨到 29 条、外加一条人工确认的 Critical,而且两次的新增意见大多由上一轮修复引出。这正是"机制的表面积增长快于可被加固的速度"的典型特征。第二种形状是三行代码。本 PR 就是第二种,因此它与我的独立方案一致,并且落在这个已经两次发散的岔路的收敛分支上。我没有找到它漏掉的更简路径。 有一点我想明确说出来,因为把连着四个相关 PR 读成"数量压力"、从而看不出这一个在性质上不同,是很容易的:前三个都在增加机制——清单解析器、标题清洗、URL 规范化、目的地校验。而这一个在删除一项要求。它不是去加固标题通道,而是把通道去掉,所以 #11490 与 #11616 的评审发散是支持它的证据,而不是反对它的证据。按它自身的价值判断,这是正确的选择;我也不认为自己是被磨到接受它的。 它解决的也是用户真能感觉到的问题。引用里编造的标题不是表面瑕疵:用户读到一个并不存在的页面名,并把它归给该发布方,而下面那条 URL 是唯一诚实的部分。这发生在 #11348 已默认开启的链路上。 如果半年后由我来维护这段代码,我会感谢写下它的人,而且理由不在修复本身。新文案自带论证——the page lists above give URLs only, so a title cannot be verified——这意味着下一个想重新加回标题的人必须与一个写明理由的判断交锋,而不是悄悄改回一个字符串。这是"一次文案改动"与"一个能留下来的决策"之间的区别。 所以我不是在代码上暂缓。让我暂缓的是两件事,我想把两件都说准确,因为它们都无法从 diff、测试或 PR 描述里解决: 1. 2. 行为层面的主张来自作者,而我在这里无法补上这个缺口。 关于模型是否真的遵守——改动后三次运行的 9/9 裸 URL、第一版文案上的 11/11、零 markdown 链接——全部是作者报告的,仅 Linux、单一模型系列。新增单测钉住的是文案,这对它们来说是正确的职责,我也确认了两条否定断言确实有效;但提示词文案改动恰恰是那种"绿色套件只证明字符串变了、对行为是否改变毫无说明"的情形。在这条路径上我不执行 PR 代码,所以能定这件事的是上面评论里的 这两点都不是 PR 的缺陷。我选择上升而不是批准,因为这里 gate 的职责是让 maintainer 来承担这个产品决策;而批准等于让它凭我对一个 maintainer 从未参与过的论证的解读就合并。 @yiliang114 —— 你已被指派,按 owner map 对 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): "agent 1c": none — all planned checks completed (~23 tool calls). I did not execute the vitest suite; the test-file conclusions above are from reading the helpers and liter….
Test Plan (not a blocker): src/tools/web-search.test.ts — no such file or directory; 105 passed — this review observed 25539, 2054, 30788, 1016, 2000, 569, 7430 passed.
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"agent 1c":none — all planned checks completed (~23 tool calls). I did not execute the vitest suite; the test-file conclusions above are from reading the helpers and liter…。
Test Plan(非阻断):src/tools/web-search.test.ts — no such file or directory; 105 passed — this review observed 25539, 2054, 30788, 1016, 2000, 569, 7430 passed。
— qwen3.8-max via Qwen Code /review (v0.23.3)
yiliang114
left a comment
There was a problem hiding this comment.
APPROVE — no Critical. CI green at b26bf068, three files, +3/-3 of production change. The two open threads are both Suggestion-severity and both forward-looking drift risks rather than defects at this commit.
The instruction now matches what the tool actually returns
WebSearchSource carries url and opened and nothing else, so there is no verified page title in the result for the model to cite. The old policy asked for "the relevant URLs from above as markdown links", which means link text — and the only title-shaped material in context is the search backend's narrated findings, i.e. untrusted external content. So the previous instruction did not merely permit an unverifiable title, it pointed the model at the one source of titles that SAFETY_FOOTER two lines above tells it to treat as data rather than as trustworthy. "Do not add titles or link text: the page lists above give URLs only, so a title (even one repeated from the narrated findings) cannot be verified" closes that, and naming the narrated-findings case explicitly is what makes it stick rather than reading as a style preference.
The example was part of the bug: it demonstrated - [cms.gov transmittal R12951CP](https://…), so a model following the example rather than the prose would produce exactly the titled markdown link the policy is trying to prevent. It is now the bare URL. Both copies of the rule — the per-result CITATION_POLICY footer at web-search.ts:632 and the CRITICAL REQUIREMENT bullet in the tool description at :838 — agree on the format and each states the reason in its own terms, and the three attribution rules that actually carry the safety weight (opened evidence pages first, an unopened candidate only when it directly supports the claim, never attach a URL that was not returned) are untouched.
The tests pin both sides with negative assertions as well as positive ones: not.toContain('as markdown links') on the result and not.toContain('](https://') on the description. That is what makes this safe to land — a partial revert that fixes one copy and not the other, or that restores the markdown example, goes red instead of shipping.
Non-blocking
R1-2 is the one I'd act on, and this PR is itself the evidence. The citation policy is hand-written twice with independently worded rationales, and the description records that the follow-up commit b26bf068 exists solely to re-word that rationale — which it had to apply to both copies in one change. The two are not equivalent in reach: the description copy sits under CRITICAL REQUIREMENT - You MUST follow this and is present on every request, while the footer rides only on a web_search result, so if they ever diverge the description wins and a relaxation applied only to the footer would be silently inert. Extracting one constant interpolated into both would remove the duplication and the asymmetry at once.
R1-1 is a fair guard but cheaper to satisfy than it sounds. Nothing pins that WebSearchSource stays {url, opened}, so the realistic regression is the withdrawn title-extraction direction (#11616): a backend author adds title, renders it into the evidence bullet, and from then on every result tells the model a title "cannot be verified" while titles sit in the list directly above that sentence. The bot is right that this is exposure rather than a present defect. A one-line assertion that the rendered page-list rows contain no title field would pin the premise the policy text asserts, and would fail loudly at the moment the premise stops holding instead of leaving the model with a contradiction.
The .qwen/e2e-tests/ note is honest about its baseline being a local build of main under an isolated HOME rather than an installed release, and names the artefact to inspect (functionResponse in ~/.qwen/projects/*/chats/*.jsonl) for step 3, which is the part a reader would otherwise have no way to check.
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
APPROVE
已核对 head b26bf0689b4b3ecdc5b1a2e6753e15e7e3a0f87c(两个提交:95ca4c60d3 改引用形态,b26bf0689b 改理由措辞;vs 已合入的 main 28df8b8a78)。改动仅 3 行产代码 + 21 行用例 + 1 份 e2e 记录。
独立复查未发现 Critical:
- 提示词里那句「上面的页面清单只给 URL」在当前实现下是真的:
buildBody的两段列表都是opened.map((source) =>- ${source.url})/candidates.map((source) =>- ${source.url})(web-search.ts:666、:675),WebSearchSource只带url与opened,没有标题字段,所以要求模型不要补标题(补了也无法核验)比原先「写成 markdown 链接」更贴近事实,也消掉了模型给不存在的标题编 link text 的路径。 - 两份引用规则(
CITATION_POLICY脚注与工具描述里的CRITICAL REQUIREMENT段)在本次改动里都被改写,且各自有独立见证。
本地验证:src/tools/web-search.test.ts 在该 head Test Files 1 passed (1) / Tests 105 passed (105)。两侧分别回退做变异:只把脚注退回 as markdown links. Add titles: → asks the model to cite bare URLs without titles 单条变红;只把描述里的示例退回 [cms.gov transmittal R12951CP](https://…) → shows a bare URL citation example in the tool description 单条变红。即本 PR 改的两处文案都有能变红的钉法,不存在「改了一半没信号」。
CI:required 全部 success(Test (ubuntu-latest, Node 22.x)、Lint & Static、Integration Tests (no-AK, No Sandbox)、web-shell E2E Smoke),无失败无 pending。
两条待回应的 Suggestion(不阻塞,记在这里以免静默丢失):
web-search.ts:632:钉的是文案,没钉住文案所依赖的事实。若后续有人给WebSearchSource加title并渲染进:666/:675的列表,提示词会变成假的而全绿。补一条列表形状断言即可闭合(例如取llmContent里那两段列表的每个条目行,断言形如- https://…整行、不含方括号链接文本或额外标题)。web-search.ts:838:同一规则手抄两处,本次就改了两遍。可让描述段的规则文本由CITATION_POLICY生成,或加一条「两处都必须包含同一个格式短语」的一致性用例,避免以后只改一处。
两条都是未来漂移防护,当前行为与用例均自洽,故按 Suggestion 记录。
|
Post-merge review — published after merge (2026-09-12). No merge gate implication. VerdictNo blocking findings. No approval blockers. Scope and tierScan — pure prompt-wording change. No logic paths touched, no persisted formats, fully reversible. Reviewed all three changed files in full. Cross-checked against the existing triage-bot review. What I checkedChange completeness (Class 10 — stated intent vs code)
No third cite-instruction site exists in Consumer coverage (Class 1 — contract asymmetry) Test quality (Class 5)
The PR description states that reverting each of the three wording changes makes the relevant test fail (3/3 caught) — consistent with the test structure above. Cross-check outcome Reviewed with AI assistance. |
What this PR does
The web_search tool now asks the model to cite sources as bare URLs, one per line, instead of markdown links. Both places that tell the model how to cite change together: the citation policy appended to every search result, and the citation requirement and example in the tool description. Both now say not to add titles or link text, and why: the result's page lists give URLs only, so a title in the answer cannot be verified — including one repeated from the search side model's narrated findings. Cite-order rules (opened pages first, candidates only when they directly support a claim, say so when attribution cannot be established) are unchanged.
Why it's needed
Asking for markdown links without supplying titles makes the model write link text itself. In practice the link text is a URL path, or a title the model reconstructs from the URL slug — which reads to the user as the page's real title. #11564 first tried to fix this by extracting real titles from the search side model's answer; that implementation (#11616) was withdrawn after two review rounds showed that turning page- or model-written text into trusted link text is an open adversarial surface (see the decision in #11564). Citing bare URLs removes the invented titles without adding a title channel to secure. The CLI (on terminals that support OSC 8 hyperlinks) and the web UI already render a bare URL as a link whose visible text is the URL itself, so the destination the user sees is the destination they get.
Reviewer Test Plan
How to verify
cd packages/core && npx vitest run src/tools/web-search.test.ts— the newWebSearchTool citationstests pin the policy wording in the tool result and the bare-URL example in the tool description.qwen -p "Search the web for the current Node.js Active LTS version, then answer with sources." --approval-mode yolo --output-format stream-json.Sources:section of bare URLs, one per line, with no[title](url)links, and every URL appears in the web_search tool result.Evidence (Before & After)
Unit tests:
npx vitest run src/tools/web-search.test.ts→ 105 passed. Reverting each of the three wording changes (policy, description bullet, description example) makes the newWebSearchTool citationstests fail (3/3 caught).eslint,prettier --check, andtsc --noEmitforpackages/coreare clean.Live runs, same prompt and isolated
HOME(Token Planqwen3.6-plus, web search on by default). TheSources:sections below are copied from each run's final answer; provenance is checked against the web_search result recorded in the session transcript.Before (
mainwording, 2 runs) — the result's policy says "as markdown links"; every citation is a markdown link, with link text the model wrote:The page lists in these runs' web_search results carry URLs only (8 and 16). In the first run, the search side model's narration inside the result did give its own link text for two of these pages — "Node.js Version Support: EOL Dates and Latest Releases" for HeroDevs and "Node.js" for endoflife.date — and the answer used neither: "Node.js End of Life Dates - HeroDevs" is rebuilt from the URL slug, and "EndofLife.date — Node.js" does not appear in the result.
After (this branch at
b26bf0689b, 3 runs) — the result's policy says "as bare URLs"; 9 of 9 citations are bare URLs, 0 markdown links:In every run, each cited URL appears in that run's web_search result (13, 12, and 9 URLs for the After runs). Three earlier runs on the first revision of the wording (
95ca4c60d3) gave the same result: 11 of 11 bare URLs, all from the result.Tested on
Environment (optional)
Local build (
packages/coreandpackages/clidist) run with an isolatedHOMEwhose settings declare one ModelStudio Token PlanmodelProvidersentry (qwen3.6-plus, key viaBAILIAN_TOKEN_PLAN_API_KEY) and notools.webSearch, so the default opt-out path is exercised.Risk & Scope
feat/web-search-titles-readonly).Linked Issues
Closes #11564. Supersedes #11616 (closed). Follows #11490.
中文说明
这个 PR 做了什么
web_search 工具现在要求模型以裸 URL(每行一个)引用来源,而不是 markdown 链接。告诉模型如何引用的两处同时修改:附加在每次搜索结果后的引用策略,以及工具描述中的引用要求和示例。两处都说明不要添加标题或链接文字,并说明原因:结果中的页面列表只提供 URL,因此答案中的标题无法核实——包括从搜索侧模型的叙述中照搬的标题。引用顺序规则(优先引用已打开的页面,候选页面仅在直接支持论断时引用,无法确定出处时如实说明)保持不变。
为什么需要
要求 markdown 链接却不提供标题,会让模型自己写链接文字。实际效果是链接文字为 URL 路径,或者是模型根据 URL 路径片段拼出来的标题——在用户看来就像页面的真实标题。#11564 最初尝试从搜索侧模型的答案中提取真实标题来解决;该实现(#11616)在两轮评审后撤回,因为评审表明把网页或模型写下的文字变成可信的链接文字本身就是一个开放的对抗面(决定见 #11564)。改用裸 URL 引用消除了编造的标题,而且不会新增需要防护的标题通道。CLI(在支持 OSC 8 超链接的终端上)和 Web 界面本来就会把裸 URL 渲染为可见文字就是 URL 本身的链接,用户看到的地址就是实际打开的地址。
评审测试计划
如何验证
cd packages/core && npx vitest run src/tools/web-search.test.ts—— 新增的WebSearchTool citations测试固定了工具结果中的策略文案和工具描述中的裸 URL 示例。qwen -p "Search the web for the current Node.js Active LTS version, then answer with sources." --approval-mode yolo --output-format stream-json。Sources:小节结尾,其中是每行一个的裸 URL,没有[标题](url)链接,且每个 URL 都出现在 web_search 工具结果中。证据(改动前后)
单元测试:
npx vitest run src/tools/web-search.test.ts→ 105 个通过。分别还原三处文案改动(策略、描述要点、描述示例),新增的WebSearchTool citations测试都会失败(3/3 被捕获)。packages/core的eslint、prettier --check和tsc --noEmit均无问题。实机运行使用相同提示词和隔离的
HOME(Token Planqwen3.6-plus,web search 默认开启)。下面的Sources:小节复制自每次运行的最终答案;出处依据会话记录中保存的 web_search 结果核对。改动前(
main的文案,2 次运行)——结果中的策略写的是 "as markdown links";每条引用都是 markdown 链接,链接文字由模型自己编写:这两次运行的 web_search 结果中,页面列表只含 URL(分别为 8 个和 16 个)。第一次运行时,结果里搜索侧模型的叙述确实为其中两个页面给出了自己的链接文字——HeroDevs 为 "Node.js Version Support: EOL Dates and Latest Releases",endoflife.date 为 "Node.js"——但答案两者都没用:"Node.js End of Life Dates - HeroDevs" 是根据 URL 路径片段拼出来的,"EndofLife.date — Node.js" 在结果中并不存在。
改动后(本分支
b26bf0689b,3 次运行)——结果中的策略写的是 "as bare URLs";9 条引用全部是裸 URL,markdown 链接为 0:每次运行中,每条被引用的 URL 都出现在该次运行的 web_search 结果中(改动后三次运行的结果分别列出 13、12、9 个 URL)。在文案第一版(
95ca4c60d3)上另跑的三次结果相同:11 条引用全部是裸 URL,且都来自结果。测试平台
Linux 已测试;macOS 与 Windows 未测试。
环境
本地构建(
packages/core与packages/cli的 dist),使用隔离的HOME,其设置只声明一个 ModelStudio Token PlanmodelProviders条目(qwen3.6-plus,key 通过BAILIAN_TOKEN_PLAN_API_KEY提供),不设置tools.webSearch,从而走默认开启(opt-out)路径。风险与范围
feat/web-search-titles-readonly分支)。关联 Issue
Closes #11564。取代 #11616(已关闭)。承接 #11490。