{ "cells": [ { "cell_type": "code", "execution_count": 13, "id": "1d2da952", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'count': 4}\n" ] }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAHAAAAFNCAIAAABXEXzbAAAQAElEQVR4nOydB1yTx//HL3sBYSl7T2UrFFfd+KvYVhR/tWK1WK1V276q1NZfq3V0Oqq11n+1VutWsNrSule1Kq0TRXDgAAGRvUJCQkKS/xcCGDAhCV4kyL3hldeTW8+Tz3P3vXvu7rmjK5VKRMAHFRGwQgTFDBEUM0RQzBBBMUMExQwdYSI9pTL3lkgokNfJlHXSFk0xCpWiVCgpVKRUNLlQKNBca/xs8G1wRM1NOJUXlUZRyJXNX1Vh4FihaJl+Y2r10anUet/m8C3jUlq2EZU0GpXBolh2Z3gGmvmEmSMcUJ6yHfpXYlH2TZFEqKDSEINNZTARlUZT1rU8R4OULQWt//GNn83uFPiNTXGg5KhFUQ9TfwPkLa9ZFbHhUwkBlC2Taj5Wd4TklQqQuK5OoZAjqQSOEdec1rOP+Qv/sUVPQfsFPbq9IOu6iMagOHqx+0Tb2DqwUWcm+3b11eOVJQ+lcC+DX+T3jW6nrO0UdMOn96lU1CfaOrCfFXq+OJNcfPNfAZtLi1/kgQzHYEGvn6s481uZf4TZ8An26Pnlt7V5hQ9qZ33rjQzEMEGFldItS3JnfutBo9HQ886tC5UnE0tnrvCg0Q34sQYIeuVk2YXDFe24aZ2atQn3pn/hyuQx9Qyvbzu0qqz2/MEupybwn0ndNy7K1T+8voLuXp7XaxgfdT18wiwcPFhblmTrGV4vQfetyWOxqX1HdUNdkjHvutSKFSl/lugTWC9BC7JrX55hh7owQQP46eeq9AmpW9Df/+8h24zSzYGHujD9XraFyvvCkVKdIXULWpQjCexrgbo83d3ZGSkCncF0CPrwrqhOBk9Ez9R63r9//+WXX0aGs2fPnkWLFiHj0O9la4lIoTOYDkHTzlWyeRT0bLl58yZqF+2OqA8OblwqHaWfq2g7mI7uu8oiGc8SWxdfK6qrq9evX3/u3Lny8vKePXuOHDkyJiYGXDZu3Ai+4eHhc+bMmThx4tmzZ48ePXr16tWqqqrAwMBp06aBFwS4d+/e66+/vnr16i+//NLKysrc3Dw1NRXcDx48uGPHDn9/f4QbJpuSkykOGtBW94UOsSCT27uxkHFYsmRJUVHRJ5984uHhAaX1m2++8fT0nDFjhlQqPXbs2IEDB+ovQCJZsGDBCy+8AIHh64kTJ0Dl5ORkGxsbBoMBLqD+pEmTQkNDAwIC4uPj3dzcVCGNAYtDE1bI2g6jQ1Do+mXxjPXYDhlq8uTJffr0geP3339/+PDhlpaWrcKw2ezExEQOh6Pyghy6d+/ea9euDRs2DHozwQWiQy5GzwQGkyaV6TCjugRVUo1nQSFbQdmsrKzs1atX3759e/TooTGYSCRau3btlStXSksbWy0VFY8NmbZYxkAJvdxyHWF0NZsoSolYd9XWPhYvXhwXF/fvv/8mJCRERUWtW7eurq6uVZjCwkIwmjKZ7Ouvv4aQ58+fbxWAxTKWRXoSaPDQGTrC6MihLA61urwOGQcLC4u33nprypQpaWlpp06d2rRpE1Qsb7zxhnqY48ePg0kFswilHrXMm88esbDO1lnHwIQOQS3tGKX5UmQEoMo+cuTI6NGjwUqGNpCZmXn79u0ng4HuKjWBkydPoo6jVqx09tEhqI4i7xfOqxXpMhvtgk6nb9iwYd68eZA9y8rKoK0DaoKs4OXq6grm8vTp0zk5OT4+PnC8b98+sAb//PPPxYsXoXYCO6AxTRcXl4yMjEuXLkE7DOFGWCWDMb6I4TZtB6OBIWvDG4beLh4t55hR7Vwxj8ExmcygoCAo0Zs3b4aqKS8v7+2334Z2KNTdtra20ETfsmULaDd+/Hi5XL5r1641a9ZAeZ8/f35NTc327dtB5eDg4KSkpOjoaGdnZ1Wa0BqFRuvu3bsjIyObHXFxeGuBsKIuYoR128F099jvWpYDnVdTFrdnxOp5Yv28+16hvChdI2m6O0fGzXYSVRml1Hcibl6sktcpo/QYl9T9WMlk0fm29O1fPZg0311jADB22uwGn8+HWkWjF5Tu2bNnI+MAKUPjHxl4SdCWGDRokEavM7+XuAdykB7oO0j3fwn3Rr5l5xmoYb4KVBdisVhjLGg/qh4QnwTcoX5HxgHsLFheZOAlQVsCqson3U/uLriXJnpnqV7jafp2fPQfbXV0a9HMFRoEhYuA9iMyJbhcLsLHrYsiGDnXM7C+g3Shg2xc/bmbPruPuhjrPr73YoyN/vMQDJvocOty5amk0lkruspg8to59/47x8nOVS/rqcLgqTgHtzzKvVETNcnOO9i0ijleziWXXPu7ashr1gF9rQ2K2J7JYhn/Vp79rdTChj7xf+7ouaMgp+bI5kJJjSJ+viuHr++EkWbaP51x1/KcikKZhS09qD8/dNDzMAfv3B9Fd1JrxCK5oyd7zKx2Pmg97YTbPatyywulCiX0S1G4FgwOl8Y2oyoUjztRoRcYulTlal2A9S6IIlc7LwRQKJtmLTccq4IpG6+PgtTmN9dPYq7v+G7t2Jg4UouubHJXUuAPKVtO9IWQcoVYVCcWymuq5DLomqMjB0/O6BlO6Cl4WkFVZKULbl8RVhZKxQ3jglLJ4zQpDSgUCjWX1udtnOLcOE28yUt9XnJD5y6VQm1KUKlKr6WglMYJzC2jN+kP52iaKt4UjUZHVJqSa07v5swOHWLRzRFDYwuPoMYGukJgnAOG6pDJY6wRTbzAw5jGZxgThAiKGSIoZoigmCGCYoYIihkiKGY6x1W20StsapAcihkiKGaIoJghgmKGVEqYITkUM0RQzBBBMUNsKGZIDsUMERQzRFDMdI6VxYigmCGVEmaIDcUMuwHUGegcgtbU1EilRnldCjtkuUvMEEExQwTFDBEUM0RQzBBBMUMExQwRFDNEUMwQQTFDBMUMERQzRFDMEEExQwTFjEm/+DV69Gi4PLlcLhAImEwmj8eDYyqVun//fmSqmHQHs62tbWpqqurlf7FYXFVVBfoOHz4cmTAmXeQnTpxobd3idXUbG5spU6YgE8akBR06dKi7u7u6S2hoqDFWWsWIqVdKcXFxzSuy2NvbT506FZk2pi7osGHDvLy8VMchISF+fn7ItOkEzaYJEyZA/W5nZzdp0iRk8uiu5WW1irICaY2ww1Zr83bsH+AxFGp8Zp1LVoYIdQQUhMys6NZ2TBpdx4q/OtqhZ34ruXdNyOPTOWadYwTfSLA41LKCWgqF4h9hFjakrQVW2hL08OYCKwd2QN/nbZO0p+HfA8V8G3rkS1rXHtIq6PGdRZZ2LP8IS0RoyfmDxTb2jF5DNeczzZVSUZ5EIlYQNTXSZ1T3u1eFslrNlYpmQcsLpHQG6TfRCpTq8iLNOwRoVk0kqLO0NXhRra6DrSNboGXxdM11t0KO5HWdYPmhjqJWIkdaVvcn5RozRFDMEEExQwTFDBEUM0RQzBBBMUMExQwRFDNEUMwQQTFDBMWM0QX9PXnPN8uMtb2uCWL0kaLMTCNur2uCYBM0N/fB5i3rr6VdgTGVgIDg11+bHBQUOjthelpa/fa6x44d/Gn9Dl8f/xs3rm/dtuH27Rt8S6u+fV58c/J0GCKGAPM/S2DQGW5uHolJ2xQKhaeH90dzF3p7++o8b0rK35BgTm42n2/p7e33wfvz7Ozqt5GKGTt8SvyMqqpK8OVwOBHhfd97d66NjS1qeFl80y8/nr9wrri4MDAwdMzo1/r0GYAwgafIS6VS0I5Goy1b+sPKFevoNPr8BXMkEsnqVRt69AgcMWLUqZOXQc2H+XlzP54lqZWs/WHzF0u+zcq6OydhumovT4hy9dplODhyKGXrln3WNrYLFiZo2xOpmctXLixc/BGkvyfx0KLPlhYVFaxes1TlxWAwkpK2UanU5N9Pbt28Lz3j2patP6m81vywfO++XWNixu/auX/QwGGLlnz89xlsmzHiETQvL6eiojx27ARQzcvLZ9HCpUuWrHhy19MTJw5DNgQpXV3d3d0953742d17medSTqt8pdLaSW9Mg6FaRwcnyFxFRYXp6dfaPu8vm9cNfHHouNg4yJ5QLGbNTDh//tztJiPj5OTyxsS3zM3MIWNCDr1z5xY41tbWHj12IG5C/KuvxPIt+NEjRw8b+tK27T8jTOAR1NnZ1dLSaunyxTt2/pKRkQb5Iiw03MzMrFWwGzfS/P0D4MervtrbOzg6Ol9Pv6r66uHh3bxsg7OTK3xCQW77vJDHIcHmr36+PeET7Inqq6/v411+zc0tRCIhHICsUJ5A32av0JDeWVn3qgRVCAd4bCiLxfr+u58PHkqGogTmCWSKnzw9Kiq6VTChsBqyz5Bh4eqOFeVlqgM26/GaDar1G1QSaEMoFEJ2Y6nFUk0rq6lpnF1CoVA0xaqGz/c/aD3pDC4DMix6arBVSlCKZ86YDUU1NfXi4SN/fr10oZu7J1gA9TBgGaGmgjDqjnyLxgyrLh/YX1R/n9paFkMlOox3N7uIGqS0sbZtI5aNbTf4/DBhPhgEdffu3XXviKgPeASFKv7GzesjX3oVfmS/fgMjI/u/FN0fClcrQb08fY4dPxgS3AtsgsrlwYMsMBeq4/tZd6FSVhkElb3z9PRu46RgH/x8e0CzodlFdezp5dNGLDAmqv2pwSipXMD6Q8sE1zZ2eGyoQFC1fMXn69avhnocKqiduzZDjRQYEIIaaoZbtzJSr16C6x43biI0idb+uBIyIAT7acOat6aNz8q+p0rEwoIP9a+gWgD/UEtA6yc4KKzt80JNDXXavn27IQo0En5ct6pXWISPd1tTHkG4+DffgfShxgNjCvU7NDxWf78UYQJPDg0MDEmY8ym0S/b8ugO+hveOXLVyPdTjcPzKqLGQ3T76+F1oUYH7po1JiYlb35n5BmRqqE8+mvtZcy6Gtqe7u9dr40eCZXSwd/zy81U6t9aDBlNJaXHSr9vhJsENCO/d5+1p7yFdvD5+speX767ELWCdeDyzgJ7BH364AGFC89ymi0fLpRIUMtiw7diehkWLP4bqYuW361Bn4My+Qt9QM59eZk96kc4RzJj6rM9XXh2szWvevMUD+g9GJoapCLpk8XKN7hs27NIWxcry2Vkk/TH1HAq1E+pUEBuKGSIoZoigmCGCYoYIihkiKGaIoJghgmKGCIoZzU9KbC5NIVcgghY4PBqdqfktWs05lG9LL3ggRgQt5NwW2Thqfo9Ls6DOPlypuMPe5zZxBGVSWwemhbXmjQo0C0qjUyJfsj62LR8RWgL98aeSCl8c201bgLZe786/Lz66rTB0kLWlHauLvy8PA9KCcml1uezf/SVvLnQzt9K6j4aOBQiElXWpf1UUPpCIqzvSAsjq6mCQna5riMl4cC3oNAbF0ZPdJ9qm7ZAmvbJYM8uXL3dzcxs/fjwyeUg7FDNEUMwQQTFDBMUMERQzRFDMEEExQwTFDBEUM0RQzBBBMUMExQwRFDNEUMwQQTFDBMUMERQzRFDMEEExQwTFDBEUByKwsAAAD5FJREFUM0RQzBBBMdM55oNYWlpyOBzUGegcObSyslIs7hyzAUmRxwwRFDNEUMwQQTFDBMUMERQzRFDMEEExQwTFDBEUM0RQzBBBMUMExQwRFDNEUMyY9Itf48ePp9PpCoWipKSExWJBN7OigaSkJGSqmHqPfWZmZvNxUVGRXC4PCwtDJoxJF/nY2FgGo8VrqlZWVlOnTkUmjEkLOnbsWA8PD3UXPz+//v37IxPGpAUFAxoTE6NaMhnVrylsER8fj0wbU6/lx4wZ4+LSuJy3v79/ZGQkMm1MXVCwoePGjYNMam5uPnnyZGTy6FvLS8WKWknHrJMTNeTVfUmH7OzsAvzCqyvq0DMHWpY8Pp1Go+gTWHc79OrpiutnqygUilLeRbefpjMplaUyezd2yCBL7xCztgPrEPT03hKFEvlHWLaxykYXQVAuvXys1DOQF9S/rR0u2hL0ZGIxi0sLGaRjlY0uxZl9hU5e7NBBltoCaK2UHt6tUSgQUbMVA2Ptc2/XiARaTblWQUvya2l00nWigTqZsuyRVJuv1lpeLJTbOrAQ4Qns3DiCCpk2X615UCJSyOq6aLXeNpIaRZ1UqzKkUGOGCIoZIihmiKCYIYJihgiKGSIoZoigmCGCYoYIihkiKGaIoJh5FoL+nrznm2WLkHEwauLt4FlMxcls2vC90yXeDnAKmpv7YPOW9dfSrsCwSkBA8OuvTQ4KCp2dMD0tLRV8jx07+NP6Hb4+/keO7v9z/77s7HseHt5Dh4yIHTtBtW/5/M8SGHSGm5tHYtI2hULh6eH90dyF3t6+bZxRPfG4uCl79+48uP+MapP6Vd99vf/Ab79sTPLw8IKvcMZ167/b/8dp8N22fePRYwdKS4u7d7cPDek9Z/YnzZtfPz3YEpJKpfDzaDTasqU/rFyxjk6jz18wRyKRrF61oUePwBEjRp06eRnUPHHyyLLlS+Bg144/p019d+++XWt/XKlKAaJcvXYZDo4cStm6ZZ+1je2ChQlyeVvr56snHj1yNFzD3bu3VV7pGdfs7Oxv3Gzc2zvjRlp47z6gJtzy5D/2zHxn9t5fj059a9bpv4//uncnwgc2QfPycioqyiG7gVheXj6LFi5dsmRFXV3rsZdDh5KDg8Nmf/A/KyvrXmERU96ckZy8ByKqfKXS2klvTIMM6+jgNCV+RlFRYXr6NT0vwMnRuVlBSDAnJ3tE1Kjr6VdVvhnp13r1eqFaWL07cSucYsCAweZm5oMHDR8TM37Hzk1PXme7wSaos7OrpaXV0uWLd+z8JSMjDQpRWGi4mVmLUWwoyJBTIsL7NruEhUWAY/PPBiOgKrCoYSN4+MzJzUZ607tXJJwaDiBBH28/SPxmw47zJSXFBYWPwntHwl2XyWSQqZuj+Pr2EAqFBQXYdj3BZkNZLNb33/188FAylOJNv/zo6OgcP3l6VFS0ehgokvB7wBf+1d2bcyibxW52ZLPrj0UiIdIbUPCHtSvgIC3tSlBQWM8eQYVFBaAmmPXu3e1cXNzOpZxudRYOhwufYgm21Q1wVkquru4zZ8yGopqaevHwkT+/XrrQzd2zeft41KARl8uFkjhw4DD1iI4OzqoDdfnA/qL6+8RGehMR0VcgqILMCDl08qS34R77+fUEY5qRca1X2AsQgMerLzHq8tXUiODT2grbaDm2Ig9VPIiIGlTr12/g4kXLoPDeuXOrVTAvL18wZGANVP+BASE21raQfVS+97PuVlVVqo5VcT09vZHe8C343l6+/6T8ff/+3ZDgXuASFBiann71SurF8PA+qrNDtXnjRlpzlFu3MsCYWlubnqCQNZav+Hzd+tUP8/PAVO3ctRksPegFXk5OLnDdqVcvQdF+e+p7KSmnDx3+A0wnVDiff/FJwtwZYApUiVhY8Nf8sFxQLYD/bdt/hkomOEjHBHD1xFFDqf/t90R3d08+v35yB1zAhQsp+fl5YEDr0ze3iBoeDVb+n3/OwCmgsfV7ctK4cRNNsdkUGBiSMOfTEycPT5o8ZnJ8LOSLVSvXww8Dr1dGjYWK+6OP34UMCC3TDet3Xr9+dUxs1NyPZ0EZ//KLVc1TaqHt6e7u9dr4kaNjhhYWPvry81U0XVv+qCcOX6Hl8Kggv/k2wOnAAkAFpdIXeHfWh/37Dfriq09jx43YuXtz3IQpcRPiET60zm36K6mY353t28sCPSsWLf5YKKxe+e06ZNpcPFJqY0/XNr2JdI5gphMshPXKq4O1ec2bt3hA/8HIlDAhQZcsXq7RfcOGXdqiWFlaIxOjE+RQB3tH1HkgNhQzRFDMEEExQwTFDBEUM0RQzBBBMUMExQwRFDNan5Q4PBqdodfrol0NNo/GYGpVRmsO5VrQSh9KEOEJCrNq+LZa33zVKqidK6tO1jHvc5s4NDrq5qz1lTitgtq7c3jmtEtHShBBjZO7HvmFm7M4WscRdLzeffFoeXmR1C/c0saRpZow0zWRSRWVxbWpJ8rChlh6Bbf1yrzuBQhuXxJcP1slqqqTyTrsTUWFQgl3s6PuKJNFldTInX04YYOtnLx1bPig98piStRRS2QA33//vYuLy9ixY1GHoFSyuDQ9w+rdwUxBLE7HNVqpMipd3pEXoDekYY8ZIihmiKCYIYJihgiKGSIoZoigmCGCYoYIihkiKGaIoJghgmKGCIoZIihmiKCYIYJihgiKGSIoZoigmCGCYoYIihkiKGY6wXtKqGEbJS6XizoDnUNQgUDA4/FQZ6BzCEqn0zGus2JUiKCYIYJipnPU8kRQzJAijxkiKGaIoJghgmKGCIoZGo3W9rqXpgPJoZghgmKGCIoZIihmiKCYIbU8ZvR+k64jGDduXE1NDUgpboDBYMCxTCZLTU1FpopJd46MHTu2tLS0rKwMZIUbL5VKQVBvbwPWvH32mLSgEyZMcHd3V3ehUqnR0dHIhDFpQSkUSlxcHJPJbHYBfWNjY5EJY+r9oTExMR4eHqpj0DcqKsrc3ByZMJ2ggzk+Pl41huzs7Nxhb3jrTScQFHKll1f9BilDhgyxsTH17dmxNZsUCsXxHYWFD2pFArlC1WRUIqWuJRgo9aF0Axep13IOyoYUddEchEqjcC2o7gHcwbF2CBMYBE1PqbhwuEIiUtAYiMFmsPlMnhWHxaGDBlQKtV6JplOoHze6KKkUikKl65O+jwVXE56idp+UYFfV70izoG3fKCVVLpeKRbWSSllNlUQuVSjqlBbWtKg37Rxcn3Z+ytMKunFBdq1YzrVkeYR3poV9W1FTLcnPKK0VyiysaJM/80BPQfsFPZVUeOO8kGvN9gx3QM8Ld1IeSkWyqDe6+/Vu5wL+7RT0wM+PHtyu6THEVedOCJ2OikJBfnrZwFjb4P6W7Yjens6Rk4lFuZniwOFPVTRMFit7C/g/+1s2WOHgAQZranAO/WNdXv6D2p6Dn0811ck4lh0ZzY+I6mZQLMPaodfPVebd7RJqAj2GuV44VIUMxDBBz/5e6tjT1JvWuIDqwcKO+9P/7hkUywBBf12dS2fRrJ2e3f41HY5riJ28Dp3eW6R/FAMELc6VuoQaZlCeAyxdLG5dqNY/vL6CHvg5n8qgcs05yCS5ln5i7meRQlEFwo2jrw0MvqSe1jdlfQV9lC0xszFRNY0Nm8tIP1upZ2B9BZVKlM6BXa68q+A78aor9B0i1Kthf/FoGQUZcfnOB7nXj53amPfwphnPqoffgBFDprHZ9e98pJz/9fjfv8x8a922xE+KirMc7LwH9psQ0etlVawDR364nHaIxeSGBf+nu60rMhrd3K2K7lSKBDKeBUNnYL1yaOEDCZVuLDVLy/J+2vK+TFb73vSNb8YtKyi6u+6XmXJ5/Sg8jc4Qi6uTD377WsynKz4/Hxw4dE/ylxWVheD1z8V9/1zcO3bURx+8s9nGyvH4qU3IyGRe1qtq0ktQYWUdhWqsrujUtCN0GiN+wjK7bu723T3/O3p+fkFmxq2/Vb4wbBw1ZJqbSxCUj/DQUfBcl19wB9zP/bsnOGAYSMzlWkCe9fYMR8YECmfZI6k+IfWSqU7W0PFoHKC8uzj35PEan5qtrRxsrJ2zcx7vMe3qFKA64HLqm8BiSTXIWlqeZ9f98QObs6M/MiY0GlWm35rpetlQGpOKRMhIiCXCvPyb0OhRdxRUlzUfP2m7JbUihULOYj3uDGYyjdsCgU5t0FSfkHoJyuFSqstlyDiYm9t4uIX+Z+h0dUcej99GFDaLR6XSZLLH+xbUSmuQMYEyYW6jl1Z6BbJ1YBXm6GVB2oGjnc+VtEOe7mHN++kWFmd1s2mr1oY8a2Xp8CA3fVD/RpdbmSnImCgUyNVXr9ERvbKxfx9zhdxYU6CgJQQDfH8e/k4qlRSX5Bw4unbl2riCIh1dEiGBw9NvnoIHJDj+6+y2nIcZyGhUlwipFOTsg0/Q7k4cGo1SnIX/wQ6Aanrue7uYDM7q9W8uX/Na1oPU/8bM11nJDB80JbL36ORDK8H4QvZ8deRs1FAwkREoza1mcfVt5OjbwZy4MkdQrvQd4IK6HjdPZfuE8qLi9Bo601f4wf/tJhV3jimveKkqESnqkJ5qIv3HlOxduWZ8WtalR54RmoeLK6uKv107QaMXh2UmrhVqTrab53vTf0b4WPDVMG1e8PRFo2n4va7OAdPfXKMtVuHtUnt3rXvTPIkBY0rV5dJtX+UGaBmbg8utEhRr9ILahslka/SiUumW/O4IH+UVj7R5SWW1TIYGaeh0poW5rcYolQXVj26WzvrWgBmpBox6mlsznbxZt04/6DHY/UlfuPnWVh0/1wHvNTy6WRY+wrCBT8Oe0GNmujCZ1JwrBagLcDcl19qR/sIIW4NiGdzlMfULD4lQknkuFz3XQM3O4VFeT3BDBtLOmSMbPs2iMunekU7oeeT23zndnJix7zsjw2n/3KbNi7MlIoVrLzue5fMzNFKcXV58t8rWmfX6h+1scT/V7LsTuwszLwkZHLpzkC2X37llLc+vKrpTDv3aL8ZYhgw0zG6qg2F+aOLK3LJ8KZVGZZkzLB141s581Hkoya6seCSQiuVIgVx82aNntqeYq4NtBvOxHQW5dyTSmqbpyxTVdNjGrsyGSbJN3ZqUhhnJreYaq+bcNgVrPGicN6s2MZnSmFwTTT3fysYwqnm7zV+bLgM1/sqG2boUGlIqGn53gyOLR/GPsHhxNJ4hSPxv0lVViPNu1wrK6molCtrjvuHHorSaqaxxGrdSJZRKiabpyGrDBsqWsZ+YsKx1KrOyofNPwWDTzG3p/mHmNAbm6Zgm/WpiZ4Qsd4kZIihmiKCYIYJihgiKGSIoZv4fAAD//8HL48cAAAAGSURBVAMAB911bc+vZeIAAAAASUVORK5CYII=", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from langgraph.graph import StateGraph, START, END\n", "from typing import TypedDict\n", "\n", "# ============================================================\n", "# 第 1 步:定义 State —— 描述整个工作流在运行时的数据结构\n", "# ============================================================\n", "\n", "class CounterState(TypedDict):\n", " \"\"\"工作流的状态:一个简单的计数器\"\"\"\n", " count: int\n", "\n", "# ============================================================\n", "# 第 2 步:写节点函数 —— 每个节点做一件事\n", "# ============================================================\n", "\n", "def step_one(state: CounterState) -> dict:\n", " \"\"\"第一个处理节点: count + 1\"\"\"\n", " new_count = state[\"count\"] + 1\n", " return {\n", " \"count\": new_count\n", " }\n", "\n", "def step_two(state: CounterState) -> dict:\n", " \"\"\"第二个处理节点: count * 2\"\"\"\n", " new_count = state[\"count\"] * 2\n", " return {\n", " \"count\": new_count\n", " }\n", "\n", "# ============================================================\n", "# 第 3 步:创建 StateGraph 对象\n", "# ============================================================\n", "\n", "\n", "builder = StateGraph(CounterState)\n", "\n", "# ============================================================\n", "# 第 4 步:注册节点,连接边\n", "# ============================================================\n", "\n", "\n", "builder.add_node(\"step_one\", step_one)\n", "builder.add_node(\"step_two\", step_two)\n", "\n", "builder.add_edge(START, \"step_one\")\n", "builder.add_edge(\"step_one\", \"step_two\")\n", "builder.add_edge(\"step_two\", END)\n", "\n", "graph = builder.compile()\n", "\n", "result = graph.invoke({\"count\":1,\"log\":[]})\n", "print(result)\n", "\n", "# 显示图结构\n", "from IPython.display import Image,display\n", "display(Image(graph.get_graph().draw_mermaid_png()))\n" ] }, { "cell_type": "code", "execution_count": 14, "id": "2abea702", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "当前 count = 1,执行 + 1\n", "count未到达 2,继续循环\n", "当前 count = 2,执行 + 1\n", "count未到达 3,继续循环\n", "当前 count = 3,执行 + 1\n", "count未到达 4,继续循环\n", "当前 count = 4,执行 + 1\n", "count已到达 5,停止循环\n", "最终结果:{'count': 5}\n" ] }, { "data": { "image/png": "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R2p63NBnHq9JSKQ7PZNSqQwIMu3bcY2B9vK/92CRUvM8j77BWAP88X7/+0uZTPooP+3P49s+3zYzJ+9h40f16hEWd/csDNbD8pkL+9Iy4wk14sxyFpuxFWHl2FraX0GBHWFzmdz7+r/kAqDrF/nKAR7XasvOuZ9tnZqcemNK+LtNNtXDhs7v33di1F+fQ6MNiq4JY1cQrQ+d6YM4u8zG3khn/DctDd5O+Mt/sguy5Z1CzfkO1YYkRKf1HeYYMsaRoNZpsPyfvNxTJrHECyOFaWXQccVs6UVjVYDIjZd0JbvDAO23nxeX5G3eNlPrJiu+jaRK+yVhdxf/VxZ/TfTnvQ0jGtoEVwLYbC3/QF+vwEURWxo6qiC1uGOQhV7L17sm7rXfvirJr4+XlYOWS28KhaKigfthqqtlHI72m3ng760ZUNWL0tKChjbJFTIuW8uvAZeerIXax0Fy7hWV5Zcv/tiPIH1oogHbe6hjbHRmtxE+9Tex2Ww7O8M/4qzf36Ews3TOasyW3jTR9x44XiRy5z24kEkswN0zqT0H2ts743iE3jT9Uk6PbG/vwr4bnU7M2p3TaZ372Q15Eaec0Cddn9L+cXNGcb6867M+xOwoFeTeudS+I0T9sbeob82YYyJqe3Z2isTW3carm/m8xZOv51YUSoJHOQ0Yi9nSv+bNkJMaLznxQ45Sydi52Xh2FRFTlhqbJxFLOTzWSxt8CaKjJfN7RR8puH+jTF6l5PLYAjuBvYu1jbMVm0fx5mm9kElVpbllZfmV0kq5slrB5bEGjnMJDMXr1hS1fHbC9ETJ/06KC3NlMomiZuK/2okBmScno1RfFax/36t6xssnV2smt/x3lkvV0yd8ek3tXISM6slT1Z+gkGFYDIsolSr4yuWzPXytRk51FzR2BxDSD719KoekUiWrePImC+iVKp+IjWYq1Mfh+mdeVc23pM6ErHVXPj7VPwuPVzPqf0CdY2sPqVmhnj3zn/PDoDDHhi2gewMi0gI/sgpRhEOIiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44UownghijBeiCKMF6II44Uownghiihc6xYAAAAVSURBVDBeiCKMF6II44Uownghiv4PAAD//20L7hYAAAAGSURBVAMAS7G0gFP6yOkAAAAASUVORK5CYII=", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from langgraph.graph import StateGraph, START ,END\n", "from typing import TypedDict\n", "\n", "class CountState(TypedDict):\n", " count: int\n", "\n", "def increment(state: CountState) -> dict:\n", " \"\"\"每次调用 count + 1\"\"\"\n", " print(f\"当前 count = {state['count']},执行 + 1\")\n", " return {\"count\":state[\"count\"]+1}\n", "\n", "def router(state: CountState) -> str:\n", " \"\"\"\n", " 条件路由函数 —— LangGraph 的\"方向控制器\"\n", "\n", " 返回 END 意味着图执行结束\n", " 返回节点名意味着跳转到那个节点继续执行\n", "\n", " 注意:返回的必须是节点名(字符串),不是函数引用\n", " \"\"\"\n", " if state[\"count\"] >= 5:\n", " print(f\"count已到达 {state['count']},停止循环\")\n", " return \"quit\"\n", " else:\n", " print(f\"count未到达 {state['count']},继续循环\")\n", " return \"incr\" # 回到increment 节点\n", "\n", "# 构图\n", "builder = StateGraph(CountState)\n", "builder.add_node(\"increment\", increment)\n", "\n", "builder.add_edge(START, \"increment\")\n", "\n", "builder.add_conditional_edges(\n", " \"increment\", # 从哪个节点出发\n", " router, # 条件路由函数\n", " {\"incr\": \"increment\", \"quit\": END} #返回值 -》目标节点的映射\n", ")\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({\"count\":1})\n", "print(f\"最终结果:{result}\")\n", "\n", "from IPython.display import Image,display\n", "display(Image(graph.get_graph().draw_mermaid_png()))" ] }, { "cell_type": "code", "execution_count": 15, "id": "cebdfb11", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['init', 'x1', 'x2', 'y1', 'z1', 'z2', 'z3']\n" ] }, { "data": { "image/png": "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", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from typing import Annotated, TypedDict\n", "from operator import add\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class AppendState(TypedDict):\n", " \"\"\"Annotated[list, add] 告诉 Langgraph:\n", " 当多个节点都要更新items时, 用add(即(list + list2))合并\"\"\"\n", " items: Annotated[list[str], add]\n", "\n", "def node_x(state: AppendState) -> dict:\n", " return {\"items\": [\"x1\", \"x2\"]}\n", "\n", "def node_y(state: AppendState) -> dict:\n", " return {\"items\": [\"y1\"]}\n", "\n", "def node_z(state: AppendState) -> dict:\n", " return {\"items\": [\"z1\", \"z2\", \"z3\"]}\n", "\n", "builder = StateGraph(AppendState)\n", "builder.add_node(\"x\", node_x)\n", "builder.add_node(\"y\", node_y)\n", "builder.add_node(\"z\", node_z)\n", "\n", "builder.add_edge(START, \"x\")\n", "builder.add_edge(\"x\", \"y\")\n", "builder.add_edge(\"y\", \"z\")\n", "builder.add_edge(\"z\", END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({\"items\": [\"init\"]})\n", "print(result[\"items\"])\n", "\n", "from IPython.display import Image,display\n", "display(Image(graph.get_graph().draw_mermaid_png()))" ] }, { "cell_type": "code", "execution_count": 16, "id": "2938b4e9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[HumanMessage(content='你好,我是王琪', additional_kwargs={}, response_metadata={}, id='f6da4a6b-e9c4-4fe6-a316-4f7e3c3697cb'), AIMessage(content='你好,王琪!很高兴认识你。有什么我可以帮你的吗?无论是学习、工作、生活中的问题,还是随便聊聊,我都在这里随时为你服务。😊', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 372, 'prompt_tokens': 15, 'total_tokens': 387, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 333, 'rejected_prediction_tokens': None, 'text_tokens': 372}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': None, 'text_tokens': 15}}, 'model_provider': 'openai', 'model_name': 'qwen3.6-plus', 'system_fingerprint': None, 'id': 'chatcmpl-fc21ad6e-3186-9c08-8aa2-66e12961da60', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f9209-e1d7-7ea2-b47b-e72a291c8afc-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 15, 'output_tokens': 372, 'total_tokens': 387, 'input_token_details': {}, 'output_token_details': {'reasoning': 333}})]\n" ] } ], "source": [ "from langgraph.graph import MessagesState\n", "from langgraph.graph import StateGraph, START, END\n", "from langchain_openai import ChatOpenAI\n", "from langchain_core.messages import HumanMessage\n", "from dotenv import load_dotenv\n", "import os\n", "\n", "load_dotenv()\n", "api_key = os.getenv('QWEN_API_KEY')\n", "\n", "model = ChatOpenAI(\n", " model=os.getenv('QWEN_MODEL'),\n", " api_key=api_key,\n", " base_url=os.getenv('QWEN_API_BASE'),\n", ")\n", "\n", "def chat_node(state: MessagesState) -> dict:\n", " \"\"\"调用LLM 返回的消息会自动追加到历史里\"\"\"\n", " response = model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", "builder = StateGraph(MessagesState)\n", "builder.add_node(\"chat\", chat_node)\n", "builder.add_edge(START, \"chat\")\n", "builder.add_edge(\"chat\", END)\n", "\n", "graph = builder.compile()\n", "\n", "# 执行: 传入一条 HumanMessage\n", "result = graph.invoke({\"messages\": [HumanMessage(content=\"你好,我是王琪\")]})\n", "print(result['messages'])" ] }, { "cell_type": "code", "execution_count": 17, "id": "72689032", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['reset_data', 'cherry']\n" ] } ], "source": [ "from typing import Annotated, TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class ReplaceableList(list):\n", " \"\"\"\n", " 自定义列表子类,带一个 replace 标记。\n", "\n", " 用法:\n", " - 普通 list → 追加到已有数据\n", " - ReplaceableList([...], replace=True) → 全量覆盖已有数据\n", " \"\"\"\n", " def __init__(self, *args, replace: bool = False, **kwargs):\n", " super().__init__(*args, **kwargs)\n", " self.replace = replace\n", "\n", "def smart_merge(current: list, incoming: list) -> list:\n", " \"\"\"\n", " 智能合并函数 —— 作为自定义 Reducer 传给 Annotated。\n", "\n", " 签名必须满足 (旧值, 新值) → 合并后的值。\n", " LangGraph 会在节点返回时自动调用此函数。\n", " \"\"\"\n", " # 检查传入的是否是 ReplaceableList 且标记了 replace=True\n", " if isinstance(incoming, ReplaceableList) and getattr(incoming, \"replace\", False):\n", " return list(incoming) # 覆盖模式:直接用新数据替换\n", " \n", " return current + list(incoming) # 追加模式:新数据拼到旧数据后面\n", "class SmartState(TypedDict):\n", " items: Annotated[list[str], smart_merge] # ← 使用自定义 Reducer\n", "\n", "\n", "def add_some(state: SmartState) -> dict:\n", " \"\"\"普通列表 → 追加\"\"\"\n", " return {\"items\": [\"apple\", \"banana\"]}\n", "\n", "def replace_all(state: SmartState) -> dict:\n", " \"\"\"带 replace 标记的列表 → 全覆盖\"\"\"\n", " new_list = ReplaceableList([\"reset_data\"])\n", " new_list.replace = True\n", " return {\"items\": new_list}\n", "\n", "def add_more(state: SmartState) -> dict:\n", " \"\"\"再追加一条\"\"\"\n", " return {\"items\": [\"cherry\"]}\n", "\n", "\n", "builder = StateGraph(SmartState)\n", "builder.add_node(\"add_some\", add_some)\n", "builder.add_node(\"replace_all\", replace_all)\n", "builder.add_node(\"add_more\", add_more)\n", "\n", "builder.add_edge(START, \"add_some\")\n", "builder.add_edge(\"add_some\", \"replace_all\")\n", "builder.add_edge(\"replace_all\", \"add_more\")\n", "builder.add_edge(\"add_more\", END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({\"items\": [\"init\"]})\n", "print(result[\"items\"])\n" ] }, { "cell_type": "code", "execution_count": 18, "id": "2f8eea8f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "开始: 拿出杯子\n", "1. 研磨咖啡豆\n", "2. 冲泡咖啡\n", "3. 加牛奶\n", "结束: 品尝咖啡\n" ] } ], "source": [ "from typing import Annotated, TypedDict\n", "from operator import add\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class CoffeeState(TypedDict):\n", " logs: Annotated[list[str], add] # 记录每一步操作\n", "# 子图使用独立的状态 schema,避免共享主图的 logs\n", "# 子图只关心自己内部产生的日志\n", "\n", "class SubCoffeeState(TypedDict):\n", " sub_logs: Annotated[list[str], add]\n", "\n", "# 子图:泡咖啡(内部3个步骤)\n", "\n", "def grind_beans(state: SubCoffeeState) -> dict:\n", " return {\"sub_logs\": [\"1. 研磨咖啡豆\"]}\n", "\n", "def brew_coffee(state: SubCoffeeState) -> dict:\n", " return {\"sub_logs\": [\"2. 冲泡咖啡\"]}\n", "\n", "def add_milk(state: SubCoffeeState) -> dict:\n", " return {\"sub_logs\": [\"3. 加牛奶\"]}\n", "\n", "# 构建子图 - 使用独立的SubCoffeeState\n", "coffee_builder = StateGraph(SubCoffeeState)\n", "coffee_builder.add_node(\"grind\", grind_beans)\n", "coffee_builder.add_node(\"brew\", brew_coffee)\n", "coffee_builder.add_node(\"add_milk\", add_milk)\n", "\n", "coffee_builder.add_edge(START, \"grind\")\n", "coffee_builder.add_edge(\"grind\", \"brew\")\n", "coffee_builder.add_edge(\"brew\", \"add_milk\")\n", "coffee_builder.add_edge(\"add_milk\", END)\n", "\n", "coffee_subgraph = coffee_builder.compile()\n", "\n", "# 主图:完整的泡咖啡的过程\n", "\n", "def prepare_cup(state: CoffeeState) -> dict:\n", " return {\"logs\": [\"开始: 拿出杯子\"]}\n", "\n", "def make_coffee(state: CoffeeState) -> dict:\n", " # 子图使用自己的状态,初始 sub_logs 为空\n", " sub_result = coffee_subgraph.invoke({\"sub_logs\": []})\n", " # 把子图的 sub_logs 映射回主图的 logs\n", " return {\"logs\": sub_result[\"sub_logs\"]}\n", "\n", "def enjoy(state: CoffeeState) -> dict:\n", " return {\"logs\": [\"结束: 品尝咖啡\"]}\n", "\n", "# 构建主图\n", "main_builder = StateGraph(CoffeeState)\n", "main_builder.add_node(\"prepare\", prepare_cup)\n", "main_builder.add_node(\"make\", make_coffee) # 使用包装函数(内部做状态映射)\n", "main_builder.add_node(\"enjoy\", enjoy)\n", "\n", "main_builder.add_edge(START, \"prepare\")\n", "main_builder.add_edge(\"prepare\", \"make\")\n", "main_builder.add_edge(\"make\", \"enjoy\")\n", "main_builder.add_edge(\"enjoy\", END)\n", "\n", "main_graph = main_builder.compile()\n", "\n", "# 运行\n", "\n", "if __name__ == \"__main__\":\n", " result = main_graph.invoke({\"logs\": []})\n", "\n", " for step in result[\"logs\"]:\n", " print(step)" ] }, { "cell_type": "code", "execution_count": 20, "id": "2d9108d5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[parse] 收到问题: 帮我对比一下苹果、华为和小米的旗舰机型\n", "[parse] 解析出 3 个品牌: ['苹果', '华为', '小米']\n", " [research] 并行查询 [苹果] -> iPhone 16 Pro Max | A18 Pro 芯片 | 6.9吋 OLED | 4K 120fps 视频 | iOS 18\n", " [research] 并行查询 [华为] -> Mate 70 Pro+ | 麒麟 9100 | 6.8吋 OLED | 物理可变光圈 | 鸿蒙 NEXT\n", " [research] 并行查询 [小米] -> 小米 15 Ultra | 骁龙 8 Gen4 | 6.73吋 AMOLED | 徕卡光学 | 澎湃 OS 2.0\n", "\n", "[summarize] 开始汇总 3 份报告...\n", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n", " >> 旗舰手机对比总结\n", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n", "\n", " 【苹果】iPhone 16 Pro Max | A18 Pro 芯片 | 6.9吋 OLED | 4K 120fps 视频 | iOS 18\n", " 【华为】Mate 70 Pro+ | 麒麟 9100 | 6.8吋 OLED | 物理可变光圈 | 鸿蒙 NEXT\n", " 【小米】小米 15 Ultra | 骁龙 8 Gen4 | 6.73吋 AMOLED | 徕卡光学 | 澎湃 OS 2.0\n", "\n", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n", "[总结] 三款旗舰各有千秋 --\n", " 苹果 iPhone 16 Pro Max:视频拍摄王者,生态闭环体验最佳\n", " 华为 Mate 70 Pro+:影像系统物理可变光圈独树一帜\n", " 小米 15 Ultra:徕卡光学加持,性价比旗舰首选\n", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n", "\n" ] } ], "source": [ "\"\"\"\n", "图的拓扑结构:\n", "\n", " START\n", " │\n", " ▼\n", " [parse_query] ← 节点:解析用户输入,提取品牌列表\n", " │\n", " ▼\n", " route_to_research ← 条件边路由函数:返回 N 个 Send\n", " │\n", " ├─ Send(\"research_brand\", {brand: \"苹果\"})\n", " ├─ Send(\"research_brand\", {brand: \"华为\"}) ← 三个并行\n", " └─ Send(\"research_brand\", {brand: \"小米\"})\n", " │\n", " ▼ (并行执行)\n", " [research_brand] × 3 ← 每个品牌独立研究\n", " │\n", " ▼ (全部完成后汇总)\n", " [summarize] ← 节点:汇总所有报告,输出对比\n", " │\n", " ▼\n", " END\n", "\"\"\"\n", "from typing import Annotated, TypedDict\n", "from operator import add\n", "from langgraph.graph import StateGraph, START, END\n", "from langgraph.types import Send\n", "\n", "\n", "# ══════════════════════════════════════════════════════════════\n", "# State 定义\n", "# ══════════════════════════════════════════════════════════════\n", "class CompareState(TypedDict):\n", " \"\"\"手机对比任务的 State\"\"\"\n", " query: str # 用户的原始问题\n", " brands: list[str] # 从 query 中解析出的品牌列表\n", " reports: Annotated[list[str], add] # 每个品牌的研究报告(add reducer 自动合并)\n", " comparison: str # 最终的对比总结\n", "\n", "\n", "# ══════════════════════════════════════════════════════════════\n", "# 节点函数\n", "# ══════════════════════════════════════════════════════════════\n", "def parse_query(state: CompareState) -> dict:\n", " \"\"\"\n", " 节点 ①:解析用户输入,提取出要对比的品牌列表。\n", "\n", " 实际场景中这里会用 LLM 做实体提取,这里直接硬编码模拟。\n", " \"\"\"\n", " query = state[\"query\"]\n", " print(f\"[parse] 收到问题: {query}\")\n", "\n", " # 模拟 LLM 解析:从自然语言中提取品牌名称\n", " brands = [\"苹果\", \"华为\", \"小米\"]\n", " print(f\"[parse] 解析出 {len(brands)} 个品牌: {brands}\")\n", "\n", " return {\"brands\": brands}\n", "\n", "\n", "def research_brand(state: CompareState) -> dict:\n", " \"\"\"\n", " 节点 ②:研究单个品牌(被 Send 并行调用的目标节点)。\n", "\n", " 它不知道总共有几个品牌,也不关心其他品牌在做什么——\n", " 只专注于自己拿到的这一个。\n", "\n", " 实际场景中这里会调搜索引擎 API 或 LLM,这里用数据模拟。\n", " \"\"\"\n", " brand = state[\"brands\"][0] # Send 保证这里只有一个品牌\n", "\n", " # 模拟:查询该品牌的旗舰机信息\n", " phone_db = {\n", " \"苹果\": \"iPhone 16 Pro Max | A18 Pro 芯片 | 6.9吋 OLED | 4K 120fps 视频 | iOS 18\",\n", " \"华为\": \"Mate 70 Pro+ | 麒麟 9100 | 6.8吋 OLED | 物理可变光圈 | 鸿蒙 NEXT\",\n", " \"小米\": \"小米 15 Ultra | 骁龙 8 Gen4 | 6.73吋 AMOLED | 徕卡光学 | 澎湃 OS 2.0\",\n", " }\n", "\n", " info = phone_db.get(brand, f\"{brand}旗舰机信息暂缺\")\n", " report = f\"【{brand}】{info}\"\n", "\n", " print(f\" [research] 并行查询 [{brand}] -> {info}\")\n", " return {\"reports\": [report]} # add reducer 自动拼到总 reports 里\n", "\n", "\n", "def summarize(state: CompareState) -> dict:\n", " \"\"\"\n", " 节点 ③:所有品牌都研究完了,汇总生成对比结论。\n", "\n", " 实际场景中这里会把所有 reports 喂给 LLM,让它生成对比分析。\n", " \"\"\"\n", " print(f\"\\n[summarize] 开始汇总 {len(state['reports'])} 份报告...\")\n", "\n", " # 模拟 LLM 生成的对比总结\n", " comparison = (\n", " \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\"\n", " \" >> 旗舰手机对比总结\\n\"\n", " \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\\n\"\n", " )\n", " for report in state[\"reports\"]:\n", " comparison += f\" {report}\\n\"\n", "\n", " comparison += (\n", " \"\\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\"\n", " \"[总结] 三款旗舰各有千秋 --\\n\"\n", " \" 苹果 iPhone 16 Pro Max:视频拍摄王者,生态闭环体验最佳\\n\"\n", " \" 华为 Mate 70 Pro+:影像系统物理可变光圈独树一帜\\n\"\n", " \" 小米 15 Ultra:徕卡光学加持,性价比旗舰首选\\n\"\n", " \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\"\n", " )\n", "\n", " return {\"comparison\": comparison}\n", "\n", "\n", "# ══════════════════════════════════════════════════════════════\n", "# 路由函数 —— 核心!返回 Send 列表而不是 dict\n", "# ══════════════════════════════════════════════════════════════\n", "def route_to_research(state: CompareState):\n", " \"\"\"\n", " ★ Send API 的核心:条件边路由函数 ★\n", "\n", " 关键规则:\n", " - 这个函数只能放在 add_conditional_edges 里,不能放进 add_node\n", " - 普通节点返回 dict(更新 state)\n", " - 路由函数返回 Send 列表(创建并行任务)或字符串(END / 节点名)\n", "\n", " 思想:几个品牌 → 发几个 Send → 启动几次 research_brand\n", "\n", " return [\n", " Send(\"research_brand\", {\"brands\": [\"苹果\"]}), ─┐\n", " Send(\"research_brand\", {\"brands\": [\"华为\"]}), ─┤ 全部并行执行\n", " Send(\"research_brand\", {\"brands\": [\"小米\"]}), ─┘\n", " ]\n", "\n", " 每个 Send 的两个参数:\n", " 参数 1: 目标节点名 —— \"派给谁做\"\n", " 参数 2: 此分支专属的 state —— \"这个任务要什么数据\"\n", " \"\"\"\n", " if not state[\"brands\"]:\n", " return END\n", "\n", " return [\n", " Send(\"research_brand\", {\"brands\": [brand]})\n", " for brand in state[\"brands\"]\n", " ]\n", "\n", "\n", "# ══════════════════════════════════════════════════════════════\n", "# 构图\n", "# ══════════════════════════════════════════════════════════════\n", "builder = StateGraph(CompareState)\n", "\n", "builder.add_node(\"parse_query\", parse_query)\n", "builder.add_node(\"research_brand\", research_brand)\n", "builder.add_node(\"summarize\", summarize)\n", "\n", "# 边\n", "builder.add_edge(START, \"parse_query\")\n", "builder.add_conditional_edges(\"parse_query\", route_to_research) # 路由 → 动态 fan-out\n", "builder.add_edge(\"research_brand\", \"summarize\") # 所有并行结果汇聚到 summarize\n", "builder.add_edge(\"summarize\", END)\n", "\n", "graph = builder.compile()\n", "\n", "\n", "# ══════════════════════════════════════════════════════════════\n", "# 运行\n", "# ══════════════════════════════════════════════════════════════\n", "if __name__ == \"__main__\":\n", " result = graph.invoke({\n", " \"query\": \"帮我对比一下苹果、华为和小米的旗舰机型\",\n", " \"brands\": [],\n", " \"reports\": [],\n", " \"comparison\": \"\",\n", " })\n", "\n", " print(result[\"comparison\"])" ] }, { "cell_type": "code", "execution_count": 21, "id": "e087f983", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "第一轮: 你好,老王!很高兴认识你。👋\n", "\n", "作为 Python 程序员,你平时主要在做哪个方向呢?比如 Web 后端(Django/FastAPI/Flask)、数据分析/科学计算、自动化运维/爬虫、AI/机器学习,还是其他领域?\n", "\n", "如果你遇到以下任何情况,随时丢过来:\n", "- 🐍 代码报错、性能瓶颈或调试难题\n", "- 📦 依赖管理、环境配置、打包部署(venv/poetry/pyproject.toml)\n", "- 🏗️ 项目结构、架构设计、测试与 CI/CD\n", "- 🔄 Python 3.12/3.13 新特性、类型提示、异步编程最佳实践\n", "- 💡 技术选型、学习路线或职业发展交流\n", "\n", "最近在用 Python 做什么项目?或者有什么特别想探讨/卡壳的技术点吗?我随时在线帮你梳理思路。\n", "第二轮: 你叫**老王**,是一名 **Python 程序员**。 \n", "\n", "如果有代码调试、架构设计、性能优化或者任何技术上的问题,随时丢过来,我帮你一起看!🐍💻\n" ] } ], "source": [ "from langgraph.checkpoint.memory import InMemorySaver\n", "from langgraph.graph import StateGraph, START, END,MessagesState\n", "from langchain_openai import ChatOpenAI\n", "from langchain_core.messages import HumanMessage\n", "\n", "# 初始化模型\n", "from dotenv import load_dotenv\n", "import os\n", "\n", "load_dotenv()\n", "api_key = os.getenv('QWEN_API_KEY')\n", "\n", "model = ChatOpenAI(\n", " model=os.getenv('QWEN_MODEL'),\n", " api_key=api_key,\n", " base_url=os.getenv('QWEN_API_BASE'),\n", ")\n", "\n", "\n", "\n", "def chat(state: MessagesState) -> dict:\n", " \"\"\"调用 LLM 生成回复\"\"\"\n", " response = model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", "\n", "# 构建最简聊天图\n", "builder = StateGraph(MessagesState)\n", "builder.add_node(\"chat\", chat)\n", "builder.add_edge(START, \"chat\")\n", "builder.add_edge(\"chat\", END)\n", "\n", "# ★ 关键:compile 时传入 checkpointer\n", "memory = InMemorySaver()\n", "graph = builder.compile(checkpointer=memory)\n", "\n", "# ============================================================\n", "# 第一次对话\n", "# ============================================================\n", "config = {\"configurable\": {\"thread_id\": \"session-001\"}}\n", "result = graph.invoke(\n", " {\"messages\": [HumanMessage(content=\"你好,我叫老王,是一名 Python 程序员\")]},\n", " config=config, # ← 必须传入 config 来标识会话\n", ")\n", "print(\"第一轮:\", result[\"messages\"][-1].content)\n", "\n", "# ============================================================\n", "# 第二次对话(同一个 thread_id)\n", "# ============================================================\n", "result = graph.invoke(\n", " {\"messages\": [HumanMessage(content=\"我叫什么名字?做什么工作?\")]},\n", " config=config, # ← 同一个 thread_id,自动加载之前的记忆\n", ")\n", "print(\"第二轮:\", result[\"messages\"][-1].content)" ] }, { "cell_type": "code", "execution_count": 22, "id": "d8e08b2d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "session-001: 你叫**老王**。 \n", "\n", "如果有代码问题、技术探讨或者需要帮忙梳理思路,随时告诉我!🐍💻\n", "session-002: 我不知道您的名字。作为一个人工智能,我无法获取您的个人信息。如果您愿意告诉我,我很乐意记住并这样称呼您!\n" ] } ], "source": [ "# 用老王的名义问\n", "result_a = graph.invoke(\n", " {\"messages\": [HumanMessage(content=\"我叫什么?\")]},\n", " config={\"configurable\": {\"thread_id\": \"session-001\"}}, # 老王的 thread\n", ")\n", "print(\"session-001:\", result_a[\"messages\"][-1].content) # \"你叫老王\"(有记忆)\n", "\n", "# 换一个 thread_id\n", "result_b = graph.invoke(\n", " {\"messages\": [HumanMessage(content=\"我叫什么?\")]},\n", " config={\"configurable\": {\"thread_id\": \"session-002\"}}, # 全新的 thread\n", ")\n", "print(\"session-002:\", result_b[\"messages\"][-1].content) # \"我不知道\"(没记忆)" ] }, { "cell_type": "code", "execution_count": 25, "id": "57802de0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "快照 1: 6 条消息, 下一个节点: ()\n", "快照 2: 5 条消息, 下一个节点: ('chat',)\n", "快照 3: 4 条消息, 下一个节点: ('__start__',)\n", "快照 4: 4 条消息, 下一个节点: ()\n", "快照 5: 3 条消息, 下一个节点: ('chat',)\n", "快照 6: 2 条消息, 下一个节点: ('__start__',)\n", "快照 7: 2 条消息, 下一个节点: ()\n", "快照 8: 1 条消息, 下一个节点: ('chat',)\n", "快照 9: 0 条消息, 下一个节点: ('__start__',)\n" ] } ], "source": [ "# 查看 session-001 的所有历史快照\n", "history = list(graph.get_state_history(\n", " config={\"configurable\": {\"thread_id\": \"session-001\"}}\n", "))\n", "\n", "# history 按时间倒序排列,[0] 是最新状态,[-1] 是最早的状态\n", "for i, snapshot in enumerate(history):\n", " msg_count = len(snapshot.values.get(\"messages\", []))\n", " next_node = snapshot.next # 快照之后接下来要执行的节点(如果是空元组表示已结束)\n", " print(f\"快照 {i + 1}: {msg_count} 条消息, 下一个节点: {next_node}\")" ] }, { "cell_type": "code", "execution_count": 27, "id": "9df0f7e5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "好的,我记住了!你最喜欢的语言是 **Python**。在接下来的对话中,我会优先围绕 Python 为你提供帮助,无论是写代码、调试、优化、学习资源还是项目思路,随时告诉我你的需求!🐍💻\n" ] } ], "source": [ "from langgraph.checkpoint.redis import RedisSaver\n", "from langgraph.graph import StateGraph, START, END, MessagesState\n", "from langchain_openai import ChatOpenAI\n", "\n", "# 初始化模型\n", "from dotenv import load_dotenv\n", "import os\n", "\n", "load_dotenv()\n", "api_key = os.getenv('QWEN_API_KEY')\n", "\n", "model = ChatOpenAI(\n", " model=os.getenv('QWEN_MODEL'),\n", " api_key=api_key,\n", " base_url=os.getenv('QWEN_API_BASE'),\n", ")\n", "\n", "\n", "def chat(state: MessagesState) -> dict:\n", " response = model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", "\n", "# 使用 Redis 持久化\n", "REDIS_URL = \"redis://localhost:6379/0\"\n", "\n", "with RedisSaver.from_conn_string(REDIS_URL) as checkpointer:\n", " # checkpointer.setup() # 首次使用需要执行一次初始化\n", "\n", " builder = StateGraph(MessagesState)\n", " builder.add_node(\"chat\", chat)\n", " builder.add_edge(START, \"chat\")\n", " builder.add_edge(\"chat\", END)\n", "\n", " graph = builder.compile(checkpointer=checkpointer)\n", "\n", " config = {\"configurable\": {\"thread_id\": \"user-42\"}}\n", " result = graph.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"记住:我最喜欢的语言是 Python\"}]},\n", " config=config,\n", " )\n", " print(result[\"messages\"][-1].content)\n", " # 即使进程重启,只要 Redis 还在,记忆就不会丢" ] }, { "cell_type": "code", "execution_count": 28, "id": "1e90d945", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[b'checkpoint_write:user-42:__empty__:1f1871a4-f9d8-6312-8000-7d460c287542:15514895-f7b2-d38c-1196-ec3089bc6a05:0', b'checkpoint:user-42:__empty__:1f1871a4-f9ce-66e6-bfff-ad92fc4e7fd2', b'checkpoint_latest:user-42:__empty__', b'checkpoint:user-42:__empty__:1f1871a4-f9d8-6312-8000-7d460c287542', b'checkpoint_write:user-42:__empty__:1f1871a4-f9ce-66e6-bfff-ad92fc4e7fd2:191c2321-6d17-59e7-e3e2-18379de588ae:0', b'checkpoint_write:user-42:__empty__:1f1871a4-f9ce-66e6-bfff-ad92fc4e7fd2:191c2321-6d17-59e7-e3e2-18379de588ae:1', b'checkpoint:user-42:__empty__:1f1871a5-6985-66eb-8001-baf6516ff08e']\n" ] } ], "source": [ "import redis\n", "# 连接到本地Redis实例\n", "r = redis.Redis(host='localhost', port=6379, db=0)\n", "# 查看所有键\n", "keys = r.keys('*')\n", "print(keys)" ] }, { "cell_type": "code", "execution_count": 29, "id": "38434611", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " [6f56dee0-a814-4404-a344-9f99ae8b4e41] 用户叫老王,是 Python 程序员\n", " [cd26dc88-61c0-4dbe-a7f7-e4f790fa7688] 用户偏好简洁的代码风格\n", " [61b8bdc3-4b72-4c05-9e05-2403da10f75c] 用户最近在学 LangGraph\n", "[('memories', 'user_42')]\n" ] } ], "source": [ "from langgraph.store.memory import InMemoryStore\n", "import uuid\n", "\n", "# ============================================================\n", "# Store 的数据模型:\n", "# store\n", "# └── namespace: (\"memories\", \"user_42\") ← 命名空间,按用户/业务隔离\n", "# ├── key: \"uuid-1\" ← 唯一键,通常用 UUID\n", "# │ └── value: {\"data\": \"...\"} ← 实际存储的值\n", "# └── key: \"uuid-2\"\n", "# └── value: {\"data\": \"...\"}\n", "# ============================================================\n", "\n", "store = InMemoryStore()\n", "\n", "# 写入记忆\n", "ns = (\"memories\", \"user_42\")\n", "store.put(ns, str(uuid.uuid4()), {\"data\": \"用户叫老王,是 Python 程序员\"})\n", "store.put(ns, str(uuid.uuid4()), {\"data\": \"用户偏好简洁的代码风格\"})\n", "store.put(ns, str(uuid.uuid4()), {\"data\": \"用户最近在学 LangGraph\"})\n", "\n", "# 语义搜索记忆(Store 内置了向量搜索能力,不需要额外的向量数据库)\n", "results = store.search(ns, query=\"用户的名字和工作\")\n", "for item in results:\n", " print(f\" [{item.key}] {item.value['data']}\")\n", "# 输出:\n", "# [uuid-1] 用户叫老王,是 Python 程序员\n", "# [uuid-3] 用户最近在学 LangGraph\n", "\n", "# 精确获取(知道 key 时使用)\n", "# item = store.get(ns, key=\"某个uuid\")\n", "# print(item.value[\"data\"])\n", "\n", "# 列出所有命名空间\n", "all_ns = store.list_namespaces(prefix=(\"memories\",))\n", "print(list(all_ns)) # [('memories', 'user_42'), ...]" ] }, { "cell_type": "code", "execution_count": 30, "id": "55e1c47e", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "\n", "\n", "@dataclass\n", "class UserContext:\n", " \"\"\"运行时上下文:描述当前请求的\"环境信息\"。\n", "\n", " 这些信息不适合放在 State 里,因为:\n", " - user_id 是调用者身份,不是对话内容\n", " - tier 是权限级别,不是业务状态\n", " - 每次调用可能不同,但不应影响 State 的结构\n", " \"\"\"\n", " user_id: str # 谁在调用\n", " tier: str = \"free\" # 用户等级:free / premium\n", "\n", "# 在 invoke 时传入:\n", "# graph.invoke(\n", "# {\"messages\": [...]},\n", "# config={\"configurable\": {\"thread_id\": \"1\"}},\n", "# context=UserContext(user_id=\"alice\", tier=\"premium\"),\n", "# )" ] }, { "cell_type": "code", "execution_count": 31, "id": "7b21b743", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "会话1: user_42\n", "💾 已存入: 我喜欢 Python 和 LangGraph\n", "好的,我已经记下了你的偏好:**喜欢 Python 和 LangGraph**。\n", "\n", "在后续的对话中,我会优先结合这两项技术为你提供代码示例、架构建议或学习资源。如果你有具体的项目构思、调试问题,或想了解如何将 LangGraph 应用到实际工作流中,随时告诉我!\n", "\n", "会话2: user_42(换 thread)\n", "根据我之前记录的信息,您提到过自己喜欢 **Python** 和 **LangGraph**。如果还有其他偏好需要更新或补充,随时告诉我!\n", "\n", "会话3: user_99(不同用户)\n", "作为您的私人助理,我目前还没有收到过您关于个人喜好的信息。由于我们的对话是全新开始的,我暂时还不知道您喜欢什么。\n", "\n", "如果您愿意,现在可以告诉我您的兴趣爱好、饮食偏好、生活习惯或任何您希望我记住的内容。我会帮您妥善记录,以便在之后的交流中为您提供更贴心、个性化的服务!\n" ] } ], "source": [ "from dataclasses import dataclass\n", "from langgraph.graph import StateGraph, MessagesState, START, END\n", "from langgraph.store.memory import InMemoryStore\n", "from langgraph.runtime import Runtime\n", "import uuid\n", "\n", "@dataclass\n", "class Context:\n", " user_id: str\n", "\n", "# ── 节点:LLM + Store 读写 ──\n", "def chat_node(state: MessagesState, runtime: Runtime[Context]):\n", " user_id = runtime.context.user_id\n", " ns = (\"memories\", user_id)\n", " last_msg = state[\"messages\"][-1].content\n", "\n", " # 搜索历史记忆,拼入 system prompt\n", " memories = runtime.store.search(ns, query=last_msg)\n", " history = \"\\n\".join([f\"- {m.value['data']}\" for m in memories]) or \"暂无\"\n", " system = f\"你是用户的私人助理。以下是对该用户的已知信息:\\n{history}\"\n", "\n", " # 调用 LLM\n", " result = model.invoke([{\"role\": \"system\", \"content\": system}] + state[\"messages\"])\n", "\n", " # 如果用户让\"记住\",写入 Store\n", " if \"记住\" in last_msg:\n", " info = last_msg.split(\"记住\", 1)[-1].strip()\n", " runtime.store.put(ns, str(uuid.uuid4()), {\"data\": info})\n", " print(f\"💾 已存入: {info}\")\n", "\n", " return {\"messages\": [result]}\n", "\n", "# ── 构图 ──\n", "store = InMemoryStore()\n", "builder = StateGraph(MessagesState, context_schema=Context)\n", "builder.add_node(\"chat\", chat_node)\n", "builder.add_edge(START, \"chat\")\n", "builder.add_edge(\"chat\", END)\n", "graph = builder.compile(store=store)\n", "\n", "# ── 演示 ──\n", "if __name__ == \"__main__\":\n", " config = {\"configurable\": {\"thread_id\": \"thread-42\"}}\n", "\n", " # 会话1: user_42 存入偏好\n", " print(\"会话1: user_42\")\n", " r1 = graph.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"记住我喜欢 Python 和 LangGraph\"}]},\n", " context=Context(user_id=\"user_42\"),\n", " config=config,\n", " )\n", " print(r1[\"messages\"][-1].content)\n", "\n", " # 会话2: 同一用户,记忆保留\n", " config = {\"configurable\": {\"thread_id\": \"thread-42\"}}\n", " print(\"\\n会话2: user_42(换 thread)\")\n", " r2 = graph.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"我之前说过我喜欢什么?\"}]},\n", " context=Context(user_id=\"user_42\"),\n", " config=config\n", " )\n", " print(r2[\"messages\"][-1].content)\n", "\n", " # 会话3: 不同用户,记忆隔离\n", " print(\"\\n会话3: user_99(不同用户)\")\n", " r3 = graph.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"我之前说过我喜欢什么?\"}]},\n", " context=Context(user_id=\"user_99\"),\n", " )\n", " print(r3[\"messages\"][-1].content)" ] }, { "cell_type": "code", "execution_count": null, "id": "9dc115f0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "用户 premium_user_001 在会话 session-888 中搜索: DeepSeek 最新文档\n", "{'messages': [HumanMessage(content='搜索 DeepSeek 最新文档', additional_kwargs={}, response_metadata={}, id='35bd6c7c-fe2c-437f-8c44-6abd83ac3fe4'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 337, 'prompt_tokens': 270, 'total_tokens': 607, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 304, 'rejected_prediction_tokens': None, 'text_tokens': 337}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': None, 'text_tokens': 270}}, 'model_provider': 'openai', 'model_name': 'qwen3.6-plus', 'system_fingerprint': None, 'id': 'chatcmpl-ca9de8c5-1c29-9007-98eb-d5fa38bb7948', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f92c3-a209-75b3-bdc1-287313f00ac8-0', tool_calls=[{'name': 'search_database', 'args': {'query': 'DeepSeek 最新文档'}, 'id': 'call_4d2bf7c29dfe4a579614b117', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 270, 'output_tokens': 337, 'total_tokens': 607, 'input_token_details': {}, 'output_token_details': {'reasoning': 304}}), ToolMessage(content='content=\"\\'DeepSeek 最新文档\\' 的搜索结果(用户 premium_user_001)\" name=\\'search_database\\' tool_call_id=\\'call_4d2bf7c29dfe4a579614b117\\'', id='a031c380-250c-476c-b2d2-b0bf0dccdd7f', tool_call_id='call_4d2bf7c29dfe4a579614b117'), AIMessage(content='已为您执行搜索。系统返回的数据库结果为占位信息,建议您直接访问 DeepSeek 官方渠道获取最新、最准确的文档:\\n\\n📖 **官方文档与资源**\\n- **开发者平台 / API 文档**:`https://platform.deepseek.com/docs`(含 API 参考、鉴权方式、调用示例、计费说明等)\\n- **GitHub 开源仓库**:`https://github.com/deepseek-ai`(包含模型权重、推理代码、技术报告及微调指南)\\n- **官方博客与公告**:`https://www.deepseek.com` 或关注其官方技术博客,可获取版本更新与新功能说明\\n\\n💡 **文档常见核心模块**\\n- API 接入规范(Endpoint、Headers、Rate Limit)\\n- 模型能力对比(上下文长度、支持语言、函数调用、JSON Mode 等)\\n- SDK 快速开始(Python / Node.js / cURL 示例)\\n- 最佳实践与安全合规指南\\n\\n如果您需要查询某个具体功能(如:如何发起对话请求、如何设置温度/Top-P、如何接入流式输出、计费规则等),请告诉我具体场景,我将为您提取对应文档要点并提供可直接运行的代码示例。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 976, 'prompt_tokens': 371, 'total_tokens': 1347, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 712, 'rejected_prediction_tokens': None, 'text_tokens': 976}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': None, 'text_tokens': 371}}, 'model_provider': 'openai', 'model_name': 'qwen3.6-plus', 'system_fingerprint': None, 'id': 'chatcmpl-3038e249-ef81-9d78-85f1-adb59e460a96', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f92c3-bcaa-7bb0-9a9a-10e0871ca1e0-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 371, 'output_tokens': 976, 'total_tokens': 1347, 'input_token_details': {}, 'output_token_details': {'reasoning': 712}})]}\n" ] } ], "source": [ "from langgraph.graph import StateGraph, MessagesState, START, END\n", "from langchain_core.runnables import RunnableConfig\n", "from langchain_core.tools import tool\n", "from langchain_core.messages import ToolMessage\n", "from dotenv import load_dotenv\n", "import os\n", "load_dotenv()\n", "\n", "# ── 工具:config 由 LangGraph 自动注入 ──\n", "@tool\n", "def search_database(query: str, config: RunnableConfig) -> str:\n", " \"\"\"搜索数据库\"\"\"\n", " cfg = config.get(\"configurable\", {})\n", " user_id = cfg.get(\"user_id\", \"anonymous\")\n", " thread_id = cfg.get(\"thread_id\")\n", " print(f\"用户 {user_id} 在会话 {thread_id} 中搜索: {query}\")\n", " return f\"'{query}' 的搜索结果(用户 {user_id})\"\n", "\n", "\n", "model = ChatOpenAI(\n", " model=os.getenv('QWEN_MODEL'),\n", " api_key=os.getenv('QWEN_API_KEY'),\n", " base_url=os.getenv('QWEN_API_BASE'),\n", ").bind_tools([search_database])\n", "\n", "# ── 模型 ──\n", "# model = ChatOpenAI(\n", "# model=\"deepseek-v4-flash\",\n", "# api_key=api_key,\n", "# base_url=\"https://api.deepseek.com\",\n", "# ).bind_tools([search_database])\n", "\n", "\n", "tools_map = {\"search_database\": search_database}\n", "\n", "# ── 节点 ──\n", "def call_model(state: MessagesState):\n", " response = model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", "def call_tools(state: MessagesState, config: RunnableConfig):\n", " last_msg = state[\"messages\"][-1]\n", " results = []\n", " for tc in last_msg.tool_calls:\n", " fn = tools_map[tc[\"name\"]]\n", " results.append(ToolMessage(content=fn.invoke(tc, config=config), tool_call_id=tc[\"id\"]))\n", " return {\"messages\": results}\n", "\n", "def route(state: MessagesState):\n", " last_msg = state[\"messages\"][-1]\n", " return \"call_tools\" if hasattr(last_msg, \"tool_calls\") and last_msg.tool_calls else END\n", "\n", "# ── 构图 & 运行 ──\n", "builder = StateGraph(MessagesState)\n", "\n", "builder.add_node(\"call_model\", call_model)\n", "builder.add_node(\"call_tools\", call_tools)\n", "\n", "builder.add_edge(START, \"call_model\")\n", "builder.add_conditional_edges(\"call_model\", route, {\"call_tools\": \"call_tools\", END: END})\n", "builder.add_edge(\"call_tools\", \"call_model\")\n", "\n", "graph = builder.compile()\n", "\n", "result = graph.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"搜索 DeepSeek 最新文档\"}]},\n", " config={\"configurable\": {\"thread_id\": \"session-888\", \"user_id\": \"premium_user_001\"}},\n", ")\n", "\n", "print(result)" ] }, { "cell_type": "code", "execution_count": null, "id": "1b5b6f60", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "=== 第一轮:查询天气 ===\n", "成都今天天气晴朗,气温较高,达到了 **36°C**,湿度为 37%,风速 6 km/h。空气质量方面,AQI 为 58,等级为 **良**。\n", "\n", "关于是否适合出门跑步,建议如下:\n", "\n", "虽然空气质量不错(良),适合户外活动,但 **36°C 的气温过高**,在白天进行户外跑步容易导致中暑或脱水,体感会非常不适。\n", "\n", "**建议:**\n", "* **避开高温时段**:如果一定要跑,请尽量选择在 **清晨** 或 **傍晚/夜间** 气温较低的时候进行。\n", "* **注意防暑补水**:高温下运动出汗量大,务必携带充足的水,并注意防晒。\n", "* **考虑室内运动**:如果无法避开白天时段,建议改为在室内健身房或有空调的环境下运动。\n", "\n", "=== 第二轮:追问空气质量 ===\n", "成都当前的空气质量指数(AQI)为 **58**,等级为 **良**。\n", "\n", "这意味着空气质量总体良好,适宜进行户外活动。对于跑步等运动来说,空气质量是一个有利条件,您不必担心空气污染对呼吸道的影响(主要需注意的仍是之前提到的高温防暑问题)。\n", "\n", "=== 第三轮:测试记忆 ===\n", "您刚才问的是**成都**。\n" ] }, { "data": { "image/png": "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", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from langchain_openai import ChatOpenAI\n", "from langchain_core.tools import tool\n", "from langchain_core.messages import HumanMessage, ToolMessage\n", "from langgraph.graph import StateGraph, MessagesState, START, END\n", "from langgraph.prebuilt import ToolNode\n", "from langgraph.checkpoint.memory import InMemorySaver\n", "import requests\n", "from typing import Literal\n", "\n", "\n", "# ============================================================\n", "# 步骤 1:初始化 DeepSeek 模型\n", "# ============================================================\n", "# model = ChatOpenAI(\n", "# model=\"deepseek-v4-flash\",\n", "# api_key=api_key, # ← 替换为你的 API Key\n", "# base_url=\"https://api.deepseek.com\",\n", "# temperature=0.7, # 天气查询不需要太高创造性\n", "# )\n", "\n", "model = ChatOpenAI(\n", " model=os.getenv('QWEN_MODEL'),\n", " api_key=os.getenv('QWEN_API_KEY'),\n", " base_url=os.getenv('QWEN_API_BASE'),\n", " temperature=0.7, # 天气查询不需要太高创造性\n", ")\n", "\n", "# ============================================================\n", "# 步骤 2:定义工具函数\n", "# ============================================================\n", "@tool\n", "def get_weather(city: str) -> str:\n", " \"\"\"\n", " 查询指定城市的实时天气。\n", "\n", " 参数:\n", " city: 城市名称,支持中文或英文,如 \"北京\"、\"Shanghai\"\n", "\n", " 返回:\n", " 包含天气描述和温度的字符串\n", " \"\"\"\n", " url = f\"https://wttr.in/{city}?format=j1&lang=zh\"\n", " try:\n", " resp = requests.get(url, timeout=10)\n", " resp.raise_for_status() # HTTP 错误就抛异常\n", " data = resp.json()\n", "\n", " current = data[\"current_condition\"][0]\n", " desc = current[\"weatherDesc\"][0][\"value\"] # 天气描述,如\"晴\"\n", " temp = current[\"temp_C\"] # 当前温度(摄氏度)\n", " humidity = current[\"humidity\"] # 湿度\n", " wind = current[\"windspeedKmph\"] # 风速\n", "\n", " return (\n", " f\"城市:{city}\\n\"\n", " f\"天气:{desc}\\n\"\n", " f\"温度:{temp}°C\\n\"\n", " f\"湿度:{humidity}%\\n\"\n", " f\"风速:{wind} km/h\"\n", " )\n", " except Exception as e:\n", " return f\"查询 {city} 天气失败:{str(e)}\"\n", "\n", "\n", "@tool\n", "def get_air_quality(city: str) -> str:\n", " \"\"\"\n", " 查询指定城市的空气质量指数(AQI)。\n", "\n", " 参数:\n", " city: 城市名称,如 \"北京\"\n", "\n", " 返回:\n", " AQI 数值和等级描述\n", " \"\"\"\n", " # 这里用模拟数据演示;实际项目中替换为真实 AQI API\n", " import random\n", " aqi = random.randint(20, 250)\n", " if aqi <= 50:\n", " level = \"优\"\n", " elif aqi <= 100:\n", " level = \"良\"\n", " elif aqi <= 150:\n", " level = \"轻度污染\"\n", " elif aqi <= 200:\n", " level = \"中度污染\"\n", " else:\n", " level = \"重度污染\"\n", " return f\"{city} 当前 AQI:{aqi}({level})\"\n", "\n", "\n", "# ============================================================\n", "# 步骤 3:将工具绑定到模型\n", "# ============================================================\n", "tools = [get_weather, get_air_quality]\n", "\n", "# bind_tools 让 DeepSeek 知道有哪些工具可用\n", "# 模型回复时会自动返回 tool_calls(如果需要调用工具)或普通文本\n", "model_with_tools = model.bind_tools(tools)\n", "\n", "\n", "# ============================================================\n", "# 步骤 4:定义图节点\n", "# ============================================================\n", "def agent_node(state: MessagesState) -> dict:\n", " \"\"\"\n", " Agent 节点:调用大模型。\n", " 模型会根据用户问题和绑定的工具,决定是直接回答还是调用工具。\n", " \"\"\"\n", " response = model_with_tools.invoke(state[\"messages\"])\n", " # 返回的消息通过 add_messages Reducer 自动追加到历史\n", " return {\"messages\": [response]}\n", "\n", "\n", "# 不需要手写工具调用逻辑!ToolNode 自动处理\n", "# ToolNode 会读取最后一条消息的 tool_calls,逐个执行工具,返回 ToolMessage 列表\n", "tool_node = ToolNode(tools)\n", "\n", "\n", "def router(state: MessagesState) -> Literal[\"tools\", END]:\n", " \"\"\"\n", " 路由函数:检查模型是否想调用工具。\n", "\n", " - 如果最后一条消息包含 tool_calls → 走工具节点\n", " - 如果只是普通回复 → 结束,返回给用户\n", " \"\"\"\n", " last_msg = state[\"messages\"][-1]\n", " # AIMessage 有 tool_calls 属性,且列表非空 → 模型想调工具\n", " if hasattr(last_msg, \"tool_calls\") and last_msg.tool_calls:\n", " return \"tools\"\n", " return END\n", "\n", "\n", "# ============================================================\n", "# 步骤 5:构建并编译图\n", "# ============================================================\n", "builder = StateGraph(MessagesState)\n", "\n", "# 注册节点\n", "builder.add_node(\"agent\", agent_node) # LLM 思考节点\n", "builder.add_node(\"tools\", tool_node) # 工具执行节点\n", "\n", "# 连接边\n", "builder.set_entry_point(\"agent\") # 从 agent 开始\n", "builder.add_conditional_edges( # agent → router → tools 或 END\n", " \"agent\",\n", " router,\n", " {\"tools\": \"tools\", END: END},\n", ")\n", "builder.add_edge(\"tools\", \"agent\") # 工具结果 → 回到 agent 继续思考\n", "\n", "# 编译(带记忆)\n", "graph = builder.compile(checkpointer=InMemorySaver())\n", "\n", "\n", "# ============================================================\n", "# 步骤 6:执行\n", "# ============================================================\n", "def chat_once(user_input: str, thread_id: str = \"default\") -> str:\n", " \"\"\"封装一次对话\"\"\"\n", " config = {\"configurable\": {\"thread_id\": thread_id}}\n", " result = graph.invoke(\n", " {\"messages\": [HumanMessage(content=user_input)]},\n", " config=config,\n", " )\n", " return result[\"messages\"][-1].content\n", "\n", "\n", "# 测试:查询天气\n", "print(\"=== 第一轮:查询天气 ===\")\n", "print(chat_once(\"成都今天的天气怎么样?适合出门跑步吗?\", thread_id=\"user-001\"))\n", "\n", "print(\"\\n=== 第二轮:追问空气质量 ===\")\n", "print(chat_once(\"成都的空气质量如何?\", thread_id=\"user-001\"))\n", "\n", "print(\"\\n=== 第三轮:测试记忆 ===\")\n", "print(chat_once(\"我刚才问的是哪个城市?\", thread_id=\"user-001\"))\n", "\n", "#显示图结构\n", "from IPython.display import Image,display\n", "display(Image(graph.get_graph().draw_mermaid_png()))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3.11 (shuheAI)", "language": "python", "name": "shuheai" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.14" } }, "nbformat": 4, "nbformat_minor": 5 }