NaviGo 采用专业化多智能体模式,每个智能体负责单一规划领域。智能体通过共享的强类型状态通信,而非直接消息传递。这种设计解耦了智能体实现,支持独立测试,并确保主管路由器可以通过检查状态完整性来推理规划进度。
| 智能体 | 文件 | 职责 | 是否需要 LLM? |
|---|---|---|---|
| 需求解析器 | src/agents/requirement-parser.agent.ts |
从自然语言提取结构化旅行字段 | 是 |
| 表单补全器 | src/agents/form-completer.agent.ts |
校验完整性;组装 UserRequest 或发起追问 |
是 |
| 风险守卫 | src/agents/risk-guard.agent.ts |
通过规则 + LLM 扫描输入/输出的提示注入与不安全内容 | 是 |
| 偏好 | src/agents/preference.agent.ts |
从自由文本请求提取结构化偏好 | 是 |
| 目的地 | src/agents/destination.agent.ts |
带推荐理由的目的地候选建议 | 是 |
| 行程 | src/agents/itinerary.agent.ts |
通过 LLM 构建按天行程,获取航班与天气 | 是 |
| 预算 | src/agents/budget.agent.ts |
通过 LLM 估算总费用并标记预算问题 | 是 |
| 打包 | src/agents/packing.agent.ts |
通过 LLM 生成天气/活动感知的打包清单 | 是 |
| 计划合成器 | src/agents/plan-synthesizer.agent.ts |
通过 LLM 组装最终计划产物并应用输出护栏 | 是 |
每个智能体遵循相同的函数签名:
async (state: PlannerState, deps?: AgentDependencies): Promise<Partial<PlannerState>>
PlannerState 的只读访问withStructuredOutput 调用 LLM{},确保可安全重复调用目的:将原始自然语言请求转换为部分结构化的旅行字段。
实现(src/agents/requirement-parser.agent.ts):
输入: state.naturalLanguage
输出: parsedRequest, decisionLog
model.withStructuredOutput(ExtractedRequestSchema),每个字段均可为空。YYYY-MM-DD)、预算(数字)、IATA 代码和兴趣关键词。parsedRequest 前被过滤掉。决策日志证据:
目的:校验提取的字段是否足以组装完整的 UserRequest,或生成澄清式问题。
实现(src/agents/form-completer.agent.ts):
输入: state.parsedRequest, state.naturalLanguage
输出: userRequest(若完整), pendingQuestions(若不完整), decisionLog
model.withStructuredOutput(FormCompletionSchema) 返回 isComplete、userRequest 和 pendingQuestions。travelStartDate、travelEndDate、budget。adults=1、children=0、interests=[]、userId="anonymous"、requestText=原始自然语言)。/plan/chat/resume 恢复。决策日志证据:
目的:对抗性输入与不安全输入的第一道与最后一道防线。
实现(src/agents/risk-guard.agent.ts):
输入: state.userRequest.requestText, state.finalPlan
输出: safetyFlags, decisionLog
model.withStructuredOutput(RiskGuardSchema) 语义检测提示注入尝试与不安全输出模式。优化:若同一规划周期内前次风险守卫已产生安全标记,则跳过 LLM 扫描(避免冗余 LLM 调用,每次约 1-2 秒)。detectPromptInjection(...)(src/security/guardrails.ts)应用正则模式,并支持零宽字符与 homoglyph 归一化。规则检查始终运行(低成本)。finalPlan 存在,对摘要运行 detectUnsafeOutput(...)。BLOCKED_PROMPT_INJECTION: 或 UNSAFE_OUTPUT:。routeFromRiskGuard)将阻断请求直接路由到计划合成器以生成安全拒答。检测到的模式(规则层):
不安全输出模式:
目的:将非结构化用户意图转换为结构化偏好画像。
实现(src/agents/preference.agent.ts):
使用 model.withStructuredOutput(PreferencesSchema) 与提示词模板:
PreferencesSchema = z.object({
travelStyle: z.enum(["relaxed", "balanced", "packed"]),
prioritizedInterests: z.array(z.string().min(1)),
preferredPace: z.enum(["slow", "normal", "fast"]),
accommodationPreference: z.enum(["budget", "midrange", "premium"]),
});
若用户在请求中显式提供了 interests,则覆盖模型提取的兴趣,确保用户意图不会被静默改写。
决策日志证据:
目的:生成带支持理由的排序目的地候选。
实现(src/agents/destination.agent.ts):
使用 model.withStructuredOutput(DestinationSuggestionsSchema),候选包含:
DestinationCandidateSchema = z.object({
name: z.string(),
country: z.string(),
iataCode: z.string().regex(/^[A-Z]{3}$/).nullable(),
cityCode: z.string().regex(/^[A-Z]{3}$/).nullable(),
rationale: z.string(),
});
回退逻辑:若用户提供了 destinationHint、destinationCityCode 和 destinationIata,当该目的地未出现在生成列表中时,智能体会将其作为显式回退候选前置。最终候选列表限制为 3 条。
提示词上下文:
目的:基于真实航班与天气数据构建具体的按天行程。
实现(src/agents/itinerary.agent.ts):
若 originIata 与目的地 IATA 可用,调用 searchFlightOffers()(Duffel 集成)两次:
travelStartDatetravelEndDate航班选项通过 pickRecommendedFlightOption()(src/agents/flight-option-selection.ts)的 O(n) min-find reduce 进行排序,优先选择抵达时间不晚于旅行开始日期的航班,然后按更早抵达、更低价格、更早起飞排序。
调用 fetchWeatherRiskSummary()(Open-Meteo 集成)获取每日预报与风险等级:
使用 model.withStructuredOutput(ItineraryDraftSchema) 生成行程。提示词包含:
对 LLM 的指令:
决策日志证据:
目的:估算总旅行费用并判断是否在用户预算范围内。
实现(src/agents/budget.agent.ts):
使用 model.withStructuredOutput(BudgetAssessmentSchema),提示词包含:
LLM 返回:
BudgetAssessmentSchema = z.object({
estimatedTotal: z.number().nonnegative(),
budgetLimit: z.number().positive(),
withinBudget: z.boolean(),
optimizationTips: z.array(z.string()),
});
风险标记:BUDGET_EXCEEDED,当 estimatedTotal > budgetLimit 时触发。
决策日志证据:
目的:基于天气预报与计划活动生成上下文相关的打包清单。
实现(src/agents/packing.agent.ts):
使用 model.withStructuredOutput(PackingListSchema),提示词包含:
LLM 返回简洁、去重的必备物品打包清单。
决策日志证据:
目的:将所有智能体输出组装为最终、已校验的计划产物。
实现(src/agents/plan-synthesizer.agent.ts):
若 safetyFlags 中存在 BLOCKED_PROMPT_INJECTION,合成器生成拒答摘要而非旅行计划。
使用 model.withStructuredOutput(PlanSynthesisSchema) 生成:
然后组装最终产物:
destinationCandidates 的第一项selectedFlightOfferId 与 selectedReturnFlightOfferId(如有)itineraryDraftbudgetAssessmentpackingList返回前,合成器对生成的摘要运行 detectUnsafeOutput()。任何不安全模式都会追加到 safetyFlags。
最终计划通过 Zod 的 FinalPlanSchema 校验后才写入状态。
智能体接受依赖以支持使用 fake 和 stub 进行测试:
| 智能体 | 依赖 |
|---|---|
requirement_parser |
{ model: ChatOpenAI } |
form_completer |
{ model: ChatOpenAI } |
risk_guard |
{ model: ChatOpenAI } |
preference_agent |
{ model: ChatOpenAI } |
destination_agent |
{ model: ChatOpenAI } |
itinerary_agent |
{ model: ChatOpenAI, searchFlights, fetchWeather } |
budget_agent |
{ model: ChatOpenAI } |
packing_agent |
{ model: ChatOpenAI } |
plan_synthesizer |
{ model: ChatOpenAI } |
默认依赖使用生产实现(真实 OpenAI 模型、真实 Duffel/Open-Meteo API),测试注入 FakeStructuredChatModel 和 stub 工具函数。
每个智能体向 state.decisionLog 追加一条 DecisionLogEntry:
{
agent: string,
inputSummary: string,
keyEvidence: string[],
outputSummary: string,
riskFlags: string[],
timestamp: string, // ISO 8601
}
这创建了一个不可变的、仅追加的审计追踪,记录每次规划决策、考虑的证据以及标记的风险。日志在 API 响应中返回,并在 CLI 输出中打印。