项目进展的动机
在现在版本的Agent中,如果用户没有指定出发地就无法进行有效的旅行计划推荐。其次Agent的输出大部分都是使用Markdown格式,所以在前端应该使用markdown渲染。
在现有版本的基础上,我加入了实时定位功能,使Agent可以根据更多的信息做出更合理的旅行计划,并且使用了md渲染,使得输出内容观感更好。
实时定位功能
我使用高德地图API的逆地理编码功能实现了实时定位转城市的逻辑。
具体实现如下:
- 获取经纬度:使用浏览器的 Geolocation API 获取用户当前的经纬度信息(即纬度和经度)。
- 调用后端接口:前端将获取到的经纬度传递给后端服务。后端服务接收到坐标后,调用高德地图提供的逆地理编码 API,传入经纬度信息。
- 解析返回数据:高德 API 返回包含详细地址信息的 JSON数据,后端从中提取省份(province)和城市(city)字段,并将这两个字段组合成当前位置信息返回给前端。
- 展示结果:前端接收后端返回的当前城市信息后,即可用于显示或进一步业务逻辑处理。
代码实现如下:
在Server.js中定义逆地理编码的接口
app.get('/api/reverse-geocode', async (req, res) => {
try {
const { lat, lng } = req.query;
console.log('逆向地理编码请求:', { lat, lng });
const gaodeKey = '302001ccf43dac6f06313d9abaf20c3f'; // 请替换成你的高德API Key
const url = 'https://restapi.amap.com/v3/geocode/regeo';
const response = await axios.get(url, {
params: {
location: `${lng},${lat}`, // 高德要求传入经度,纬度
key: gaodeKey
}
});
if (response.data.status === '1') {
// 输出 province 和 city 两个字段
const addressComponent = response.data.regeocode.addressComponent;
const province = addressComponent.province || '';
const city = addressComponent.city || '';
res.json({ province, city });
} else {
throw new Error(`逆向编码失败,原因:${response.data.info}`);
}
} catch (error) {
console.error('逆向地理编码失败:', error);
res.status(500).json({ error: error.message });
}
});
定义一个从浏览器获取经纬度并且调用服务器逆地理编码接口的函数
xport async function getCurrentCityName() {
return new Promise((resolve) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords;
try {
const instance = axios.create({
baseURL: 'http://localhost:8080',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
const response = await instance.get('/api/reverse-geocode', {
params: { lat: latitude, lng: longitude }
});
if(response.status === 200 && response.data.city){
resolve(response.data.city);
} else {
resolve('未知城市');
}
} catch (error) {
console.error('获取城市名失败:', error);
resolve('未知城市');
}
},
(error) => {
console.error("获取当前位置失败:", error);
resolve('未知城市');
}
);
} else {
resolve("浏览器不支持");
}
});
}
最后在Agent.js中加入用户当前位置的提示词:
async function firstChat(input) {
try {
const cityName = await getCurrentCityName();
const today = getCurrentDate();
const response = await axiosInstance.post(
'/chat/completions',
{
model: 'deepseek-chat',
messages: [
{
role: 'user',
content: `${input} 用户当前所在城市:${cityName},请分析上面内容中出行游玩的出发地、目的地、出行及回程时间,以样例"出发地:济南-目的地:北京-出行时间:2025年5月1日-返程时间:2025年5月5日"的格式输出",并且要求输出只包含上述格式的内容,不要包含其他任何内容,当前时间为${today}`,
},
],
}
);
console.log('First Chat Output:', response.data.choices[0].message.content);
return response.data.choices[0].message.content;
} catch (error) {
console.error('Error in firstChat:', error);
if (error.response) {
console.error('Response data:', error.response.data);
console.error('Response status:', error.response.status);
}
throw error;
}
}
前端Markdown渲染
由于前端使用的是React,React库中自带Markdown渲染的库,所以只需要将聊天输出对应的div的渲染方式改为Markdown即可
具体代码如下:
import ReactMarkdown from 'react-markdown';
{/* 中间聊天区域 */}
<Content style={{ display: "flex" }}>
{/* 聊天面板,如果有详情则隐藏 */}
{!detailPlace && (
<div className="chat-panel">
<div className="chat-header">🧭 TripBot</div>
<div className="chat-history">
{messages.map((msg, idx) => (
<div key={idx} className={`chat-message ${msg.sender}`}>
<ReactMarkdown>{msg.text}</ReactMarkdown>
</div>
))}
{isLoading && (
<div className="chat-message bot loading">
<div className="chat-message bot skeleton"></div>
<div className="chat-message bot skeleton"></div>
<div className="chat-message bot skeleton"></div>
</div>
)}
</div>
<div className="chat-input">
<input
type="text"
value={input}
placeholder="Describe your outing"
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
/>
<button onClick={handleSend}>➤</button>
</div>
</div>
)}
效果示例: