行程规划 Agent
本页说明行程规划 Agent 如何结合跨会话的出行偏好和实时公开信息,生成每项都带来源的行程草案。读完本页,你能理解这个场景为什么以 Web Agent 和 GUMem 为核心、公开比价为什么不需要交互式委托,以及预订与支付为什么必须另行授权。
适用场景
用户希望 Agent 根据预算、时间、偏好和实时网页信息规划旅行、会议或出差安排。机票、酒店和活动信息分散在多个网站上且随时变化,人工比价既慢又容易过期;而饮食限制、酒店档次这类偏好每次重新问一遍,用户很快就会放弃使用。
典型触发时机:
- 假期时间确定后,需要在预算内比较多个目的地的机票和酒店组合。
- 出差日期已定,需要按时间习惯和饮食限制快速生成一份可执行行程。
- 目的地已锁定,需要对比多个出行日期组合找到价格合适的方案。
工程挑战
- 信息碎片且分钟级过期:机票价格与余位在两次查询之间就可能变化,草案里的每个报价必须带来源 URL 和查询时间,否则用户无法判断它还作不作数。
- 偏好难以跨行程延续:饮食限制、酒店档次、时间习惯不该每次重新问,但把三年前的偏好当成现在的口味同样会出错——偏好记忆需要衰减和更正机制,历史行程的满意度反馈也应该参与修正。
- 比价与预订不是同一级动作:查询公开报价是无风险的读;登录查会员价、操作账户、提交订单是完全不同等级的动作。混在同一个凭证里,意味着比价任务拿着能下单的权限。
模块组合
| 模块 | 角色 | 说明 |
|---|---|---|
| GenAuth | 核心 | 运行时以静默委托签发短时效凭证(所有产品调用必需);本场景默认无需交互式确认,查询会员价、账户操作或写动作(如预订支付)时才升级 interactive。 |
| Web Agent | 核心 | 通过 WebSearch 查询公开的交通、住宿和活动信息,每条候选项保留来源 URL 与查询时间。 |
| GUMem | 核心 | 出行偏好、饮食限制和历史行程满意度——按季度衰减的真实偏好记忆,让草案贴合当前口味而不是三年前的。 |
何时需要交互式确认
所有 Web Agent 和 GUMem 产品调用都需要 GenAuth 委托令牌;公开比价这类只读场景用静默委托签发即可,不需要用户逐次确认授权。静默签发的运行时凭证已经提供这个场景需要的约束:凭证短时效、用户可随时撤销、每次调用都带 grantId 与 auditId 可供追溯。
需要升级为 mode: 'interactive' 交互式委托、让用户在 Qoni Console 明确确认的情况:
- 登录态查询:查看会员价、里程票或账户内的优惠券,需要进入用户登录后才可见的页面。
- 账户操作与预订:提交订单、支付、取消或修改任何账户设置——这些写动作不在本页示例的委托范围内,需要另行发起带明确确认的预订任务。
注意:本页示例申请的是产品级委托(products: ['webSearch'])与 GUMem 记忆读写 scope。站点清单这类细粒度边界由 GenAuth 的 Agent Profile 或策略层配置强制执行,不由任务 prompt 承担。完整语义见 Delegate Token 与缩权。
工作流程
用户输入目的地、时间和预算上限。
应用获取静默运行时凭证,范围限定在公开网页查询与 GUMem 记忆读写。
GUMem 召回长期偏好(酒店档次、饮食限制、时间习惯)和历史行程的满意度反馈。
Web Agent 查询公开的交通、酒店和活动信息,逐项抽取价格、时间和余位,并保留来源 URL 与查询时间。
检查点:每条候选项都应带来源和查询时间;给不出来源或已过期的报价不进草案。
Agent 汇总候选项,生成行程草案,并标注与预算和偏好的匹配情况。
用户确认草案后,如需预订,另行发起一个交互式委托的预订任务,逐次确认。
检查点:预订、支付等写动作不因为草案已通过就自动执行;每次确认对应一条可回放的审计记录。
行程结束后,用户对住宿和安排的满意度反馈经确认写回 GUMem,参与下次偏好修正。
示例代码
下面的示例使用官方 Qoni SDK(@qoniai/qoni)把这个场景接到你的服务端:静默运行时凭证 → 召回出行偏好 → 一次 webSearch.run() 完成比价 → 应用侧按 JSON 契约校验 → 用户确认后写回偏好。
import { Qoni, QoniScopes } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})
interface TripOption {
item: string
kind: 'transport' | 'stay' | 'event'
price: string
sourceUrl: string
capturedAt: string
}
// 应用侧校验:解析 prompt 约定的 JSON 契约,
// 缺来源 URL 或查询时间的候选项直接丢弃
function parseOptions(output: string): TripOption[] {
const items = JSON.parse(output) as TripOption[]
return items.filter((option) => option.item && option.kind)
}
export async function planTrip(
userId: string,
destination: string,
dates: string,
budget: string,
) {
// 1. 静默委托签发运行时凭证:所有产品调用必需;
// 公开比价不需要交互式确认
const { data: grant } = await qoni.delegateToken({
user: { id: userId },
agent: 'travel-planning',
products: ['webSearch'],
scopes: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_MEMORY_WRITE],
})
// 2. 任务前:召回出行偏好与历史行程满意度
// 首次使用时,先调用 qoni.gumem.createSession 创建这个 sessionId
const { data: preferences } = await qoni.gumem.recall({
token: grant.token,
sessionId: `user-${userId}`,
query: 'budget range, hotel tier, dietary restrictions, schedule habits, past trip feedback',
})
// 3. 一次 WebSearch 完成公开比价:prompt 明确 JSON 输出契约
const search = await qoni.webSearch.run({
token: grant.token,
prompt: `
Plan a trip to ${destination} between ${dates} within budget ${budget}.
Compare public transport, hotel and event options. Return ONLY a
JSON array of options, each shaped as
{ "item": string, "kind": "transport" | "stay" | "event",
"price": string, "sourceUrl": string, "capturedAt": string }.
Drop options without a source URL.
Comparison only: do not book, pay, or sign in to any account.
Confirmed preferences from Memory: ${JSON.stringify(preferences)}
`,
maxResultsPerQuery: 8,
})
const result = await search.wait()
// 4. 应用侧校验:无来源或无查询时间的报价不进草案,并标注丢弃数量
const options = parseOptions(result.output)
const sourced = options.filter(
(option) => option.sourceUrl && option.capturedAt,
)
// 预订与支付不在本凭证范围内:用户确认草案后,
// 另行发起 mode: 'interactive' 的委托并逐次确认
return {
draft: sourced,
droppedCount: options.length - sourced.length,
audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
}
}
// 5. 由用户在界面确认后调用:只写入白名单字段
// (确认过的偏好与行程反馈文本),不写原始比价输出
export async function confirmAndRemember(
grantToken: string,
sessionId: string,
confirmed: { preferences: string[]; tripFeedback: string[] },
) {
await qoni.gumem.addMessages({
token: grantToken,
sessionId,
messages: [
{
role: 'user',
content: [
...confirmed.preferences.map((p) => `Confirmed preference: ${p}`),
...confirmed.tripFeedback.map((f) => `Trip feedback: ${f}`),
].join('\n'),
},
],
})
}行程草案的输出结构由 prompt 中的 JSON 契约约定,parseOptions 在应用侧强制执行:缺来源 URL 或查询时间的候选项直接丢弃,并在返回值中标注丢弃数量。写回 Memory 由独立的 confirmAndRemember 在用户确认后调用,只接受白名单字段。SDK 顶层只返回通用的 RunResult(runId、status、output、artifacts 等)。
记忆策略
- 进 Memory:用户确认过的出行偏好(预算区间、酒店档次、饮食限制、时间习惯)和历史行程的满意度反馈,均带时间;偏好类记忆按季度衰减,避免旧口味支配新行程。
- 不进 Memory:报价、余位等实时行情——它们分钟级过期,属于本次任务的采集数据;本次行程的临时约束(例如"这次要靠近会场")随任务结束丢弃或短期保留约两周。
- 更正:偏好变化时(例如从经济舱改为商务舱),把旧偏好标记为已失效并指向新记忆,而不是物理删除,保证可追溯。
失败处理
| 情况 | 推荐处理 |
|---|---|
| 票价页面需要登录或出现验证码 | 跳过该来源并记录;确需会员价时另行发起交互式委托,不静默绕过。 |
| 页面结构变化导致价格抽取失败 | 按失败处理,不输出无证据的价格。 |
| 候选项缺少来源或查询时间过旧 | 应用侧校验直接丢弃该条目,需要时重新查询。 |
| 召回的偏好与本次输入冲突 | 以用户本次明确表达为准,并把更正写回 GUMem。 |
生产注意点
预订、支付或取消操作需要另行发起交互式委托并逐次确认,不应默认自动执行:即使用户已批准行程草案,提交订单前仍需一次独立确认,且每次确认对应一条可回放的审计记录。报价数据有强时效性,交付草案时应标注查询时间;用户隔天再看时,价格应重新查询而不是直接复用。
下一步
- 阅读 快速开始 跑通 Agent 身份与委托的最短路径。
- 阅读 Track 了解票价与余位持续盯守的产品能力。
- 继续查看 个人研究 Agent 了解相邻场景。