{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "27a4ff30", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'count': 4}\n" ] }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAHAAAAFNCAIAAABXEXzbAAAQAElEQVR4nOydCXwTxdvHZzdp0qb3Te+To/TgKqeIViwKiJyCIKciAupfqKivcquvcogi8Apy3wKCVgXk/INCEVEKhXKXnkBpaUvv5miy75OkTUO7m2TDBBI6X7Gfzczs7OaXZ46dmX1GyDAMIuBDiAhYIYJihgiKGSIoZoigmCGCYgaboBdTSnOuVFWVKRVyRlmr7orRNKVSqQ8EQkobooWiEYXUURRNMaq6lAxiGBXShdSlpCjo1enyoShI1Diw/nSkPREyh3x00DRSqXS5oQe7iAxNUSJH2tXDLjxO0qqDK8IB9ZD90KM772ZfqpZVqeCb2IkpOxENoiAVhfS+Gy1Eqlq9S1Lq/xk9QSElUivSWA4E4foqw4lM3V/9lLrTURPVHshQe3o9KshUnRsjq2bUqJDEWRDVzbnrC17oITBf0INb8jMvVAnsKP9w+679Pb397JEtk3W5IvVo6b3bcvhJ4p526d7PG5mFmYKunpkJdtOtn0dMD3f0ZHHyl8L0vyrE9tSEeeGIP7wFvZBS+ufuojZdnJ4f2QI9uexZnleQLZu6JBLxhJ+g5SWyzZ/lTfkqTCAQoCedK/+UHt1exPfL8hA09WjR6QOlUxfz/tFsmhVJGZM+DRY5iUxMT5uYrrxE/tf+Zqcm8MIY77Xzc01Pb6qg2xfmdkjA01OzLVp2cG0RKt4wL9PE9CYJ+tPyPLED3eMlM3sSts6Qt4OgrwqtvymJTRL0TqZswGQ/1IyJe9r14slyU1IaF/SnFbfEEsqrhQNqxvQY4AVt9+nf7xlNaVzQghxpTA8X1OzxC7O/8nel0WRGBL11o0pZi7r3f6S1582bN1966SXEn127ds2dOxdZhm79ParLlUaTGRH0/J+lUN7Ro+Xy5cvILMw+0RT8QiQwynMh5b7hZEaG70oLFE5ulhozraioWLVq1cmTJ0tKStq2bdu3b99BgwZByNq1ayE2Pj5++vTpr7322okTJw4ePHju3LmysrKYmJiJEydCFCTIyMh49dVXly5d+vnnn7u7uzs7O6empkL4vn37tm7d2qZNG4QbOzGde7Um7ilDwxdGxJLVqFr4iJFlmD9/fkFBwccffxwWFgal9csvvwwPD588ebJcLj906NDevXshjVQqnTVrVpcuXSAxfDxy5AionJyc7OnpaWdnByGg/pgxY9q3bx8dHT1+/PiQkBBtSktgL6Er7isMpzEiKAziiiWWslAwqLFjx3br1g2O33333eeff97Nza1RGnt7+x07djg4OGijwEJ37959/vz53r17U+qBVQSngxWjR4JIRMsUKsNpjIpFmfwwxRswKyibpaWlHTt27N69e1RUFGuyqqqqFStWnD17tqioSBty/35DRcZ1liWAaQVKZaRFMaIWRTOKmlpkGebNmzdq1Ki//vorKSkpMTFx5cqVtbWNr3X37l2oNBUKxRdffAEpT58+3SiBWGypGqkpMLtDGxt4MmKhMKVRVmS8r2AeLi4ur7/++oQJE9LS0o4dO7Zu3TpoWEaPHq2f5vDhw1ClQrUIpR49aJuPnpoqpXegkYkJI4K6etrduyVHFgCa7AMHDgwcOBBqyfYarl27dvXq1abJQHetmsDRo0fR40NWxfhHGBHUSJFv291ZJjVSDZuHUChcvXr1Rx99BOZZXFwMfR1QE2SFqODgYKgujx8/npOT07JlSzjes2cP1AanTp06c+YMtE5QD7DmGRQUlJ6e/s8//0A/DOGmrERtWF0SPQ0nE0BFZiDa08/+zMESeyfaNxjzHJxIJIqNjYUSvWHDBmia8vLy3nzzTeiHQtvt5eUFXfSNGzeCdiNGjFAqldu3b1+2bBmU95kzZ1ZXV2/ZsgVUjouL27lzZ79+/QIDA7V5Qm8UOq0//PBD165ddYG4OLQ1v7K0tnOih+Fkxkfst36ZI69Rvf5pGGrerPnkZliso9GZNON9ouHTA6orLNUu2QqXTpfKahhT5iWNd9pF9kJnD+Gmz7LHzQ5lTQCVHVe94erqCq0KaxSU7mnTpiHLADlD5x/xvCXoSzzzzDOsUX/sKQqJNqnSM3WS7v+SMl583ScihmUcD5qLmpoa1rOg/6h9QGwKhEP7jiwD1LNQ8yKetwR9CWgqm4Yf3n4343zllEUmzaeZ+ljZc5DHwY2FU79iERRuAvqPyJqQSCQIH9f+rZyy2NQmxNTnyna9PIKjJOtm30TNjJUfZoAxmT41z2+hA8z9H99VNKXZTCavmJ7xyvQA32Ae0z+8l+LsW38n92p14mjfyDjrKuZ4Sfn13rljZQnDPaK7e/A60ZzFYumnSk/8XOTqZTfqoxD0xJGfU31gQ4FcqhzzSZDEhffIi/nLGX9YnFOSr3DxFMb2dGn/DL+f0TqBmffrqVU1lcqACPtBU8180HrYBbe7vsktuStXqZDYgXJ0sYOHVLGDgGEaBg31V8DCMfxT6a2AVa+BZerSqBfq1qekKUq9HLY+QV2gZjkypc0bojVHmmFmRpeJOhmiVHora+uygqdsAaVUag5oWqlSj2sqlSppVW1Npaq6slYhhaE1qkWo/cDJAegheFhBtWSlV1w7W1GcL5dVq1RK9bhhwwUaC9qwlLshWm9pcn1w3Y1pDzQrvlVCoUBzriZMpUmv/aPNQ7ceutEi5vqsYChTpemb6g4oGo4ZR1ehT6B9hwRXL38Maw/wCGppYCgE5jlgqg5ZPbbxFgg8jLE+w1ghRFDMEEExQwTFDBEUM0RQzBBBMWMbd2lgVNjaIBaKGSIoZoigmCGCYoY0SpghFooZIihmiKCYIXUoZoiFYoYIihkiKGYs9s4MVoigmCGNEmZIHYoZiURiudW5eLENQaVSaXV1NbIFbKQcCYVN31q0ToigmCGCYsY2uk3EQjFDBMUMERQzRFDMEEExIxAIuF41tDaIhWKGCIoZIihmiKCYIYJihgiKGZsR1Fa6TVb9Jt24ceMuXLigfbeQqnvHU/1e57lz55C1YtWjTf/5z398fHxomoaOPa0B1OzUqROyYqxaUNCubdu2+iHOzs6PzLmleVj7eOjEiRPd3Rs8yoaFhSUkJCArxtoFjY6O1pVxkUhk5eaJbGLE/o033vD19YWD0NDQPn36IOvGeCufe73qRmqFTFp/gqaxffANf82OaBrXrxonAI3dOeg8AjQ90MDU+RKouyHEPOhElqZRevqlO3fuRLVtG+Af0PQeND4dKOqB7eyQ/s00uq7OJUHTO+RCQCORA+rY28nVwwkZxIig6+ZkyKrV3rEVsvo7aNg/jmFUdber89PwwKZ6tGbLNxWbrwU95w00pTlk6gPqo3SJBQIEfVCNzpTePTTop94FT3tDDyqq87mh25mu3hdHw6+ujWq0uWBTaAFDC2iFXOXqTo+eaWgnMEOCfv9xhpe/sM/YUESoZ/fyDJFQ+Nr/hHIl4BR0zcyMwJb2PQdj9sL5BPDb99lKBTNmJrvzNvZG6a+9hSolImqyMuCt0PJi5b177PuCsAuae0Nq70w2peVEZE9d+G8VaxS7aopqFbKI5+UnBYaq4dhohV1QpQoxxjzgN2dqlQxTy64PKdeYIYKaA01x7uhBBDUHtTM9jjaGCIoZdsOlaQqRNskQFMOhD7ug6gdzG/Ax+NiA0RWKQ1BS5M1B/bhO6lCMqGtE0spjRF0jEgvFCAz7Ig7H9uyCUgKodhGBC/UgOse6Cw4LrUVKW3AlbIWwV60MxeDqiP6cvOvLhXNRs8Hidei1axbcXvdxAa28xZ/lc3OzN2xcdT7tLMypREfHvTp8bGxs+2lJk9LS1NvrHjq07/tVW1u1bHPp0oVNm1dfvXrJ1c29e7enx42d5OjoCAlmzk6yE9qFhITt2LlZpVKFh0V+MGNOZGQro9dNSfkDMszJzXJ1dYuMbP3eux/5+qq3kRo05PkJ4yeXlZVCrIODQ+f47u+8PcPT0wtpXhZft/6703+fLCy8GxPTfvDA4d269UR8UHH3QzkfPSk+JV4ul4N2AoFg4YLlSxavFAqEM2dNl0qlS79eHRUV06dP/2NH/wU1b93Om/HhVKlMumL5hs/mf5WZeWN60iTtOkU45dz5f+HgwP6UTRv3eHh6zZqTZHTF3b9n/54z7wPIf9eO/XNnLygoyF+6bIE2ys7ObufOzTRNJ/98dNOGPRfTz2/c9L02atnyRbv3bB88aMT2bb8906v33Pkf/vEnv80YQR2ufihHsGbSF5lMXl7O/fslQ4eMBNUiIlrOnbNg/vzFTVd0HjnyO5ghSBkcHBoaGj7j/dk3Mq6dTDmujZXLZWNGT4Rb9fcLAOMqKLh78eJ5w9ddv2Flr6efGzZ0FJgnFIupU5JOnz55tb6SCQgIGv3a685OzmCYYKHXr1+BQJlMdvDQ3lEjx788YKiri2u/vgN7P/fi5i1rED84m2yOZ3mG4bXMMTAw2M3NfcGieVu3rU9PTwO76NA+3smp8ZqAS5fS2rSJhi+v/diihZ+/f+CFi3VrE8PCInVuGwIDguEvFGTD1wUbhwx1H1u3Uq8sg/pE+7FVq4Zdfp2dXaqq1NNqICuUJ9BXF9W+XafMzIyy8jJkMrwfPSmK39iIWCz+9ps1+/YnQ1GC6glkGj92UmJiv0bJKisrwHwSesfrB94vKdYe2IsbfDZo/TdoJeCisrISzE2sd5Z2o6/q6qr6b0GxnVUBf999741G4XAbYLDINCi+jZIZfVAoxVMmT4Oimpp65vcDv36xYE5IaDjUAPppoGaElgrS6Ae6utQZrL58UP8i9e9kyC2GVnSptGE7vCqNlJ4eXgbO8vTyhr/vJ82ECkE/3MfH+I6IOnhbKDRKKj6aQhN/6fKFvi++DF+yR49eXbs+9WK/p6BwNRI0IrzlocP72sV1pOm63zc7OxOqC+3xzcwb0ChrKwRtfRceHmngolA/tG4VBd0GXYj2ODyipYGzoDLR7k8NlZI2BGp/qN94bWOnXnzEq1FS8bTQ8vKyRYs/XblqKbTj0EBt274BWqSY6HZI0zJcuZKeeu4fuO9hw16DLtGK75aAAUKy71cve33iiMysDG0mLi6u0P6WV5TDP2gloPcTF9vB8HWhpYY2bc+eH+AU6CR8t/Lrjh06t4xsbeAUEG78uLcgf2jxoDKF9h06Hku/XYD4AGMjPAdH1G0Sj1Y+JqZd0vRPoF+y68et8DG+U9evl6yCdhyOB/QfAub2wYdvQ48Kwtet3bljx6a3powGo4b25IMZs3VWDH3P0NCI4SP6Qs3o18L/80+/Nrq1HnSY7hUV7vxxC/xI8APEd+r25sR3kDFeHTE2IqLV9h0boXZydHSKbhv3/vuzECbY1zZt+iwb5uWHTgtBj4q58z6E5mLJVyuRLbDti5sB4ZIBb/k1jSLDd+bAv1GiKCt5K2jAy89yRX300byeTz2LHgfqbhOv8VDNJm6PdNpz/rxFrOGrV2/nOsXd7bHuLMjRblt7kYfWCVkfZJLu0cEulyN2qAAAEABJREFUqHrHRjIxbxYcyxmVDFnOaAB4UmJospwRH+pXUjieJomgmOHohwopZBuvp1sdHP3QWlKHGoZz9R0p8ubBUDbasbc52AUVOQiYWlKJciISUwIxexT7ALODI0wtEEE5kctUXgHsfvXZBU0Y7lVTSZ6U2MlKV8+PdnmBfeaKXVBXT4cWYaJtX2YgQhNSfrkX29OFK9bQ692nD9w7998yv3BJQEsHB4moaQL1mjJ9dwNaXwIUy/SJ9iX/B0JYxgcbh6lXrHGXE0ZtDuqMDeZZf28U96SO7uao+lzYLlZRLsu7Vll8S/7yVP+AMM4ZPSMOCEDTK6crpdVKpQKZCKt3BJavSjW5dQoZHZBp+sOYiMHfRu/CHPcAFxXYIQcnqudQ74i2nOaJrNwRlo7i4uKRI0ceOnQIWT1kcxXMEEExQwTFDNk1ETPEQjFDBMUMERQzRFDMEEExQwTFDBEUM0RQzJCOPWaIhWKGCIoZIihmSB2KGWKhmCGCYoYIihnb2MqXCIoZ0ihhhtShmHFxcWnqH8I6sQ1BKyoqeL3P/hghm/xhhgiKGbLZNGZso9tkQxZKBMUMqUMxQwTFDBEUM0RQzBBBMUMExQwRFDNEUMwQQTFDnuUxQywUM1b9Jt3gwYOzsrJomla/p6kBqbeNUJ07dw5ZK1Y9ODJlyhSY/ABBocjTGlfwoGxsbCyyYqxa0D59+oSHP7BVtoODw4gRI5AVY+3Dd+PGjXN2dtZ9DAoK6t+/P7JirF3QhISEtm3bao+h4A8dOhRZNzYwwKwzUjDPgQMHIuvG1G5T9qVypbLBqWuj9/Q17hLUTbC+9wGq3u+A2seRJprDV0LjrYZ0KTWhjJd9dNeYgdevX3/h6b55VxUIKfSTafw6sOesf0Wuq2tu2Ij3WQYpRWI6uLVJCwOMd5u2LcwqK1LCBZW1Bi5p6I4sF2scTJ56aaHaUFqEioe+E2Q4pRFB18+5KZZQTw/zc/d2QM2bW9fLT/xS6O4lfGVaqIFkhgRdMzPDO1jc+1Ujv0mzYve3GdAnHjsznCsBZ6P0Z3IBo6KImo0Y9l5k5X3VncwargScguZcqXbyNLLRQfNE5IDOHiniiuUUVCFDYhFxNciC0E4oreaO5YqolTEKk301NStq5Ugh52x4iA3yxvDG5kRQ3qiHvbgfMDkFpWiKto2FT48cxtAuspyCMipGpUKEpqgYGOQ2pw5lENlwmg0zi7z6GZjB8Rj8xAHmyXCXXe46lIjJAU0hmlscbgulDBl2c4bhdmKPDDdKDGmUuOBuXLiLvADRAlLsWTC8zTG3hSqRSklaeRbqFwiwY6hjT9HEQlkxNA3A2e5o6lA8Fvpz8q4vF85FlsGimXNgyEH2o3iWv1a/4bvNZc6Kugo1ox9KU4ji2RfNzc3esHHV+bSzcMXo6LhXh4+NjW0/LWlSWloqxB46tO/7VVtbtWxz4OBvv/62JysrIyws8rmEPkOHjNReaObsJDuhXUhI2I6dm1UqVXhY5Acz5kRGtjJwRf3MR42asHv3tn2//al9Efzrb774be9P69fuDAuLgI9wxZWrvvntl+MQu3nL2oOH9hYVFfr4tGjfrtP0aR/TfIYtDD8pccaoGMRrHZlcLoevJxAIFi5YvmTxSqFAOHPWdKlUuvTr1VFRMX369D929F9Q88jRAwsXzYeD7Vt/nfjG27v3bF/x3RJtDnDKufP/wsGB/SmbNu7x8PSaNSfJ8CpG/cz79R0I93DjxlVt1MX0876+LS5drtvbO/1SWnynbqAm/OTJv+ya8ta03T8efOP1qcf/OPzj7m2ID0qQhvumOAWlhfxGm/Lycu7fLwFzA7EiIlrOnbNg/vzFTdcg7t+fHBfXYdp7/+Pu7tGxQ+cJ4yYnJ++CE7WxcrlszOiJYLD+fgETxk8uKLh78eJ5E28gwD9QpyBkmJOT1Sex/4WLdev00i+e79ixS0VlxQ87NsElevZ81tnJ+dlnnh88aMTWbet4rZXUrDTgjOW20Fp+o02BgcFubu4LFs3bum19enoaFKIO7eMbeQ2AggyW0jm+uy6kQ4fOEKj72lAJ6Dw3BAao953Pyc1CJtOpY1e4NBxAhi0jW0PmlzU7zt+7V5h/9058p67wqysUCjBq3SmtWkVVVlbm5982/SqG60FsjZJYLP72mzX79idDKV63/jt//8DxYyclJvbTTwNFEr4PxMI//XCdhdqL7XWB9vbq46qqSmQyoODyFYvhIC3tbGxsh7ZRsXcL8kFNqNZ9fHyDgkJOphxvdBUHB7VfgxppjelXMVwP4nxSCg4OnTJ5GhTV1NQzvx/49YsFc0JCw3XbxyONRhKJBEpir1699U/09wvUHujLB/UvUv9O9shkOnfuXl5eBsYIFjp2zJvwG7du3RYq0/T08x07dIEEjo7qEqMvX3V1Ffz1cPc0/SqGO/bc/VCeT0rQxIOISKNajx695s1dCIX3+vUrjZJFRLSCigxqA+2/mOh2nh5eYD7a2JuZN8rKSrXH2nPDwyORybi6uEZGtDqV8sfNmzfaxXWEkNiY9hcvnjubeiY+vpv26tBsXrqUpjvlypV0qEw9PHgIigw+enIKCj0DXo0SmMaixZ+uXLX01u08qKq2bd8ANT3oBVEBAUFw36nn/oGi/eYb76SkHN//+y9QdUKD8+lnHyfNmAxVgTYTFxfXZcsXlVeUw7/NW9ZAIxMX28HwdfUzR5pS/9PPO0JDw11d3eAj3MDff6fcvp0HFag6f2eXxOf7QS1/6tSfcAnobP2cvHPYsNd4dZsMW5mBJyXEq1GKiWmXNP2TI0d/HzN28NjxQ8Euvl6yCr4YRA3oPwQKyQcfvg0GCD3T1au2XbhwbvDQxBkfToUy/vlnX0PZ1GYCfc/Q0IjhI/oOHPTc3bt3Pv/0azAow9fVzxw+Qs/hTv5t3c8Al4MaABoorb7A21Pff6rHM5/97ydDh/XZ9sOGUSMnjBo5HvHBcJHnXNu0+uNMFy9R/4mB6FExd96HlZUVS75aiaybHYuzHF0Eoz4MZo01MGJPkUF7M+AevmOs5YWbAS8/yxX10Ufzej71LLImDM4pPVoLnT9vEWv46tXbuU5xd/NAjx6D08HcFoqsBb8W/siqMDgdbMhCKUQqUd4YmKRD1lKJWhlmL3QgsGPmQgcCF+oFCzT/xWJqqyZVKBvqGRBzFosxpF/Pjpl1qPV07K0NUoc+UoigmOEU1E5MCe1ImWdBKGQEIv6NksiBUsjI8jsWVErk6My9noErIiLGsbyYvKjEgrRa1XWAK1csp6Dd+nsLRfTetdmIoMfOrzI8/ATePpzvzht5vXvLguxaWW3757wi49xQ8+biqaJLJ8oCWon7jTc0i2HcAcGPS7OL7tQqFSwDehpPDY0DTXvl/8FUTc7ROmoy8exGH5t4mzD15QtQgvWalOa5UShEgW0c+k8IMJyJqY6wykpq5FJBk5PVV2r0GEYjSkUh9EC22vtsCKLqXp1idK4wGG0ijTeNuhA9dxsV5RWzZ8/6dtm3OncbdV9ekxpyU/+n91E9ZKnxvKH+q71W/RfVOPKoy7jR/dOUxudGXZT6v/r7VTq5IAcnk1wwmNoPdfV4nB4dGKGypDLby0+ErB6yJx1miKCYIfspYcZmBCUWihMo8sRCcUKKPGaIoJghgmKGCIoZIihmiKCYIU9KmCEWihkiKGaIoJghgmKGCIoZIihmbEhQ2/B1RQTFDCnymCGCYsbZ2dnFxQXZArYhaHl5uaOjI7IFyJ50mCGCYoYIihnb6DYRQTFDijxmiKCYIYJihgiKGSIoZsju3ZghFooZIihmiKCYIYJihgiKGRtq5SlrdoQxbNiw6upqkLJGg52dHRwrFIrU1FRkrVj14MiQIUOKioqKi4tBVvjh5XI5CBoZGYmsGKsWdOTIkaGhofohNE3369cPWTFWLShFUaNGjRKJGl7xBH2HDh2KrBhrHw8dNGhQWFiY9hj0TUxMhBlQZMXYwADz+PHjJRK1t/nAwECoVZF1YwOCglVGRKg3SElISPD05OMf/XGArdsE7e/hrQX5WdLqShWj7TI2cqXQyO/Cg7FNvSmwemKg2DzFsgYiFae16HIVCCl7Rzo0WpLwii/CBAZBL5wsOXOgVFqlooVIZG/n4CaSuDmIJQIVRQmoxk4g1J4vtFfUOFww5L+CadgHSvdbsKXXuneoO2zwB6GncmPFGapWpZBVympKFdX3pbUK9e/v6CJ4YZyvX4gEPRwPK+jaWVmyGqXEXRzWycoc+/Khqqwm/3KxtFLh7CYYNycMPQTmC3rsx4JLpyokHvbh8X7oSeF6yi15lSJxtE/rTmYupTJT0F9X38m7Xh2VEEI/cVt8379bcftCUa+hXnE9zfGsZM7gyNGdBbeu10T3fqiiYbW4t3CGfyd+yoJjMzTlbaHJK/Pys2VRzz6ZauqTfjiry4uuXfp48zqLX4G9cLL01o1moSYQ9Vzwmd/LEE/4CXoyucg/6nFsbvA4gEFYF1/J6k9u8jqLh6C7vskV2Ak8Al1RsyG4na9SwRz/qcD0U3gIWpgnD2rPr0J5AnALcLlyqsL09KYKunfNbaEdLXF5nA7bDHD+4pEZs7tWVt1HuPFr7aliUOpxU3M2VdA7WTInLytV09KIJXYXT5SamNhUQeU1qhZtmktz1AhXf8eK+6ZOEZrUsf/nUBEtpCz3plB27oVDx9bm3brs5Oge1bpnn4SJ9vbqdz5STv94+I/1U15fuXnHxwWFmX6+kb16jOzc8SXtWXsPLP83bb9YJOkQ94KPVzCyGN6h7gXXSytLZU5uYqOJTbLQO5lSZLHJ0aLivO83vqtQyN6ZtHbcqIX5BTdWrp+iVKpn4QVCu5qaiuR9Xw0f9MniT0/HxTy3K/nz+6V3IerUmT2nzuwe0v+D997a4Onuf/jYOmRRKHQ9tcqUhCYJWlOhhA4TsgypaQeEArvxIxf6eoe28Al/ZeDM2/nX0q/8oY2FaePEhIkhQbEw7Bffvj88193Ovw7hJ//aFRfdGySWSFzAZiPD45ElgbHakny5KSlNElRVy+FMFwdQ3oMC2zo61j01e7j7eXoEZuU07DEdHBCtPZA4qEeAaqQVIGtRSZ6vT8MDW6B/G2RJYAxILjXJCb1J1SIjoC23pVqNtDLv9mXo9OgHllcU646bOmOWyqpUKqVY3DAYLBJZugfCCE3zymOSoA6OVEWJpXzaOzt7hoW0f+G5SfqBjo6GnsfsxY40LVAopLoQmbwaWRIwJydPkxwWmySol5/4brZJNYgZ+Pu2PJu2Pzy0g25o9W5hprenoVYbbNbdzS879+IzT9WFXLmWgiyJSskEtzZpdsSkOjSqqwuvjad5AT0hlUr16+/fyOXSwns5ew+uWLJiVH5BhuGz2sU8f/HyMXhAguP/nticcysdWYyywnKodQIj8QnqHWAvEKCCzGJkAaCZnvHOdpGdw9JV4xYtG8VBp8YAAAJlSURBVJ6ZnfrKoJlGG5nnn5nQtdPA5P1LoPIF83y57zSELLUrYXFulb3E1CcgUweYdyzJKS9hWvUMQs2PS//NatVRkjjSpFlIU4V/9hVvRY1tLHnFS/m9apjiN1FNZPqcUotgiaMrnfnPnfDO7FmXlhV+tWIka5SD2KlGVsmerXf4O5PWIHzM+t/eXFHw9CUQsHzf4MDoSeOWcZ2Vf+VeixAeGxLwmFMqL5Fu/vxWTCL7/Afcbll5IWsUtDYikT1rFE0L3Vx9ED5K7t/hipIrZCI7lodxoVDk4uzFekppfsWdy0VTv+KxIpXHeIeLh31QpOjy8ay2bHNK8ON7uD/+tQ547+HOleL4PvxmKPjNKQ2cGiwW0znn7qBmwI2UXA8/oWVnPYE3Pg2XVcivpeSgJ5rLx7LF9tSrSSGIJ2auHFn9caZAJIzoFoCeRK79keMdJB7ytjnfzvy1TRvmZ0mrmeD2Po5uT87USGFWSeGNMlBzRJKZPe6HWn13ZHv+tX+rRBJBQKy31c7fmUjxrdLCjPtKOXp6sFu7Xl7mZoNjfeiub3Lu5SkoAWXvLHbzk9jWxH1hVllpfrm8Sv3MEtRSPHDKwz4KYlvBfGhrfu41qbxGqVLWr45lHhzK1C171a6abbx1HGpY8tyQki2wLqghZ+0Oag1p6q/+wEe9EEqAGM2+adotjiXOdMuOTk8PwtMdxv8mXVlJTd41eUWxXCplBBYY52e0S5/1Q9S7yekPp2s3o0MNitYtfK77phSlEjnQTp7CqHhX7MsxrfrVRFuEbDaNGSIoZoigmCGCYoYIihkiKGb+HwAA///lU8bVAAAABklEQVQDAIah3E4aA1NaAAAAAElFTkSuQmCC", "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", "class CounterState(TypedDict):\n", " \"\"\"工作流的状态:一个简单的计数器\"\"\"\n", " count: int # 当前计数\n", "\n", "# ============================================================\n", "# 第 2 步:写节点函数 —— 每个节点做一件事\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", "\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", "# ============================================================\n", "# 第 3 步:创建 StateGraph 对象\n", "# ============================================================\n", "builder = StateGraph(CounterState)\n", "\n", "# ============================================================\n", "# 第 4 步:注册节点,连接边\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\") # START → step_one \n", "builder.add_edge(\"step_one\", \"step_two\") # step_one → step_two\n", "builder.add_edge(\"step_two\", END) # step_two → END\n", "\n", "# ============================================================\n", "# 第 5 步:编译图(编译后才可执行)\n", "# ============================================================\n", "graph = builder.compile()\n", "\n", "# ============================================================\n", "# 第 6 步:执行\n", "# ============================================================\n", "result = graph.invoke({\"count\": 1, \"log\": []})\n", "print(result)\n", "# 输出: {'count': 4}\n", "\n", "#显示图结构\n", "from IPython.display import Image,display\n", "display(Image(graph.get_graph().draw_mermaid_png()))" ] }, { "cell_type": "code", "execution_count": 15, "id": "2a12b591", "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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", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from langgraph.graph import StateGraph, START, END\n", "from typing import TypedDict\n", "\n", "\n", "class CountState(TypedDict):\n", " count: int\n", "\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", "\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", "# 构图\n", "builder = StateGraph(CountState)\n", "builder.add_node(\"increment\", increment)\n", "\n", "builder.add_edge(START, \"increment\")\n", "\n", "# 条件边:从 increment 出发,根据 router 的返回值决定走哪个目标\n", "# 第三个参数是路由目标映射 —— 告诉 LangGraph router 可能返回哪些值\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", "#显示图结构\n", "from IPython.display import Image,display\n", "display(Image(graph.get_graph().draw_mermaid_png()))" ] }, { "cell_type": "code", "execution_count": null, "id": "ffa189bf", "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", "\n", "class AppendState(TypedDict):\n", " # Annotated[list, add] 告诉 LangGraph:\n", " # 当多个节点都要更新 items 时,用 add(即 list1 + list2)合并\n", " items: Annotated[list[str], add]\n", "\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", "\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", "# 输出: ['init', 'x1', 'x2', 'y1', 'z1', 'z2', 'z3']\n", "# ↑ 初始值 ↑ x 节点 ↑ y 节点 ↑ z 节点 — 全部拼接在一起\n", "\n", "#显示图结构\n", "from IPython.display import Image,display\n", "display(Image(graph.get_graph().draw_mermaid_png()))" ] }, { "cell_type": "code", "execution_count": null, "id": "0331cce8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[HumanMessage(content='你好,我是小李', additional_kwargs={}, response_metadata={}, id='be73159e-a824-496b-81e7-3cf215c59c24'), AIMessage(content='你好,小李!很高兴认识你😊 有什么我可以帮你的吗?无论是学习、工作还是日常问题,都可以随时跟我说~', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 151, 'prompt_tokens': 8, 'total_tokens': 159, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 122, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 8}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '0170f1d9-ae86-44bf-8936-8c8fff62c31a', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f379e-1bee-7173-8285-397d7474a445-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 8, 'output_tokens': 151, 'total_tokens': 159, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 122}})]\n" ] } ], "source": [ "from langgraph.graph.message 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('OPENAI_API_KEY')\n", "\n", "# MessagesState 内部已经定义了 messages 字段,且用 add_messages 做 Reducer\n", "# 等价于:messages: Annotated[list, add_messages]\n", "\n", "model = ChatOpenAI(\n", " model=\"deepseek-v4-flash\",\n", " api_key=api_key,\n", " base_url=\"https://api.deepseek.com\",\n", ")\n", "\n", "def chat_node(state: MessagesState) -> dict:\n", " \"\"\"调用 LLM,返回的消息会自动追加到历史里\"\"\"\n", " response = model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]} # add_messages 会处理去重和合并\n", "\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": null, "id": "e37d4b58", "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", "\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", "\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", "\n", "\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", "# 输出: ['reset_data', 'cherry']\n", "# ↑ replace_all 把 init+apple+banana 全抹了\n", "# ↑ add_more 在 reset_data 后面追加 cherry" ] }, { "cell_type": "code", "execution_count": 6, "id": "eb4fa1ec", "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", "\n", "# ============================================================\n", "# 定义状态\n", "# ============================================================\n", "class CoffeeState(TypedDict):\n", " logs: Annotated[list[str], add] # 记录每一步操作\n", "\n", "# 子图使用独立的状态 schema,避免共享主图的 logs\n", "# 子图只关心自己内部产生的日志\n", "class SubCoffeeState(TypedDict):\n", " sub_logs: Annotated[list[str], add]\n", "\n", "\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", "# 主图:完整泡咖啡流程\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", "# 运行\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": null, "id": "6172964a", "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\"])\n" ] }, { "cell_type": "code", "execution_count": 8, "id": "df1ac16b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "第一轮: 你好老王!很高兴认识你,一位 Python 程序员同行 🐍。有什么编程问题、项目想法或者想聊聊技术、工具、最佳实践,随时可以聊。我在这里全力支持你!\n", "第二轮: 你叫老王,是一名 Python 程序员。\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", "model = ChatOpenAI(\n", " model=\"deepseek-v4-flash\",\n", " api_key=api_key,\n", " base_url=\"https://api.deepseek.com\",\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)\n", "# 模型能正确回答\"你叫老王,是 Python 程序员\"——因为它看到了第一轮的完整历史" ] }, { "cell_type": "code", "execution_count": 9, "id": "9a8b87c5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "session-001: 根据你之前提到的信息,你叫**老王**。\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": 10, "id": "1f2c29cf", "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}\")\n", "\n", "# 输出示例:\n", "# 快照 1: 6 条消息, 下一个节点: () ← 最终状态(已执行完毕)\n", "# 快照 2: 5 条消息, 下一个节点: ('chat',) ← chat 节点执行前\n", "# 快照 3: 4 条消息, 下一个节点: ('chat',) ← 第二轮调用的 chat 执行前\n", "# 快照 4: 2 条消息, 下一个节点: ('chat',) ← 第一轮调用的 chat 执行前\n", "# 快照 5: 1 条消息, 下一个节点: ('chat',) ← 初始输入后的状态" ] }, { "cell_type": "code", "execution_count": 3, "id": "b25c3a28", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "好的,我已经记住了!您最喜欢的编程语言是 Python。如果您有任何 Python 相关的问题、代码需求或者想聊聊这门语言,随时告诉我。\n" ] } ], "source": [ "#安装依赖:uv pip install langgraph-checkpoint-redis -i https://pypi.tuna.tsinghua.edu.cn/simple\n", "from langgraph.checkpoint.redis import RedisSaver\n", "from langgraph.graph import StateGraph, START, END, MessagesState\n", "from langchain_openai import ChatOpenAI\n", "\n", "model = ChatOpenAI(\n", " model=\"deepseek-v4-flash\",\n", " api_key=api_key,\n", " base_url=\"https://api.deepseek.com\",\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": 4, "id": "50fa207e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[b'checkpoint:user-42:__empty__:1f1790b6-5042-63f6-8000-e9d4a57835f5', b'checkpoint_write:user-42:__empty__:1f1790b6-503d-690a-bfff-70cd30804f50:54bc45c2-c409-3e1d-8283-08e60d81b5e1:0', b'checkpoint_write:user-42:__empty__:1f1790b6-503d-690a-bfff-70cd30804f50:54bc45c2-c409-3e1d-8283-08e60d81b5e1:1', b'checkpoint:user-42:__empty__:1f1790b6-6476-68c2-8001-ab699575b8b2', b'checkpoint_write:user-42:__empty__:1f1790b6-5042-63f6-8000-e9d4a57835f5:36e65fe6-dc01-ae32-4072-f4965d7b7b94:0', b'checkpoint:user-42:__empty__:1f1790b6-503d-690a-bfff-70cd30804f50', b'checkpoint_latest:user-42:__empty__']\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": 7, "id": "d4e61ada", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " [d668e3c2-cdbf-4cc1-a953-2ce817a7294c] 用户叫老王,是 Python 程序员\n", " [c51a1b78-de95-4463-9025-de6c1905bea2] 用户偏好简洁的代码风格\n", " [ab6a0ebe-ec33-468e-8d32-e421f9811bd7] 用户最近在学 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": 8, "id": "1cf92911", "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": null, "id": "12b79e01", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "会话1: user_42\n", "💾 已存入: 我喜欢 Python 和 LangGraph\n", "好的,我已经记住了!您喜欢 **Python** 和 **LangGraph**。以后在交流中,我会注意结合这些偏好为您提供更贴心的建议或答案。随时有需要,尽管告诉我!😊\n", "\n", "会话2: user_42(换 thread)\n", "您之前提到过喜欢 **Python** 和 **LangGraph**。\n", "\n", "会话3: user_99(不同用户)\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-43\"}}\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)\n" ] }, { "cell_type": "code", "execution_count": 12, "id": "b013bbad", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "用户 premium_user_001 在会话 session-888 中搜索: DeepSeek 最新文档\n", "用户 premium_user_001 在会话 session-888 中搜索: DeepSeek 文档 2024 2025\n", "{'messages': [HumanMessage(content='搜索 DeepSeek 最新文档', additional_kwargs={}, response_metadata={}, id='b708d7f6-4920-4ac9-8f0d-98a9024a3aaa'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 68, 'prompt_tokens': 276, 'total_tokens': 344, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 18, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 276}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '41015f8c-88a6-43ec-b398-c67277d489b8', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f3659-1279-72d3-b431-ec214a965bbe-0', tool_calls=[{'name': 'search_database', 'args': {'query': 'DeepSeek 最新文档'}, 'id': 'call_00_l0s4JcogGJziRVPdRWaE2646', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 276, 'output_tokens': 68, 'total_tokens': 344, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 18}}), ToolMessage(content='content=\"\\'DeepSeek 最新文档\\' 的搜索结果(用户 premium_user_001)\" name=\\'search_database\\' tool_call_id=\\'call_00_l0s4JcogGJziRVPdRWaE2646\\'', id='db8b28df-bd78-4d59-8929-a5807324d487', tool_call_id='call_00_l0s4JcogGJziRVPdRWaE2646'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 85, 'prompt_tokens': 409, 'total_tokens': 494, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 30, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 153}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '6b266eda-cb4f-4b93-94b8-df01ae0c2cb9', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f3659-1725-7322-96ea-7e58aa963cfe-0', tool_calls=[{'name': 'search_database', 'args': {'query': 'DeepSeek 文档 2024 2025'}, 'id': 'call_00_Tpu1HCRDrNZ7B0ZTtmfP9868', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 409, 'output_tokens': 85, 'total_tokens': 494, 'input_token_details': {'cache_read': 256}, 'output_token_details': {'reasoning': 30}}), ToolMessage(content='content=\"\\'DeepSeek 文档 2024 2025\\' 的搜索结果(用户 premium_user_001)\" name=\\'search_database\\' tool_call_id=\\'call_00_Tpu1HCRDrNZ7B0ZTtmfP9868\\'', id='6a85a528-c3e6-4aa7-9eec-1b3af7f44b58', tool_call_id='call_00_Tpu1HCRDrNZ7B0ZTtmfP9868'), AIMessage(content='您好!我无法直接检索到外部实时文档,但作为 DeepSeek 本身,我可以为您介绍 **DeepSeek 的最新文档和相关信息**:\\n\\n---\\n\\n## 📄 DeepSeek 最新文档概览\\n\\n### 1️⃣ **官方文档网站**\\nDeepSeek 的官方文档主要发布在:\\n- **官网文档中心**:`https://platform.deepseek.com/docs`\\n- **GitHub 仓库**:`https://github.com/deepseek-ai`\\n\\n### 2️⃣ **最新模型版本**\\n目前 DeepSeek 的最新模型是 **DeepSeek-V3** 和 **DeepSeek-R1**,相关文档包括:\\n- **API 调用指南** — 支持对话补全、流式输出等\\n- **模型参数说明** — 上下文长度、支持的语言等\\n- **费用说明** — Token 计费方式\\n\\n### 3️⃣ **API 文档主要内容**\\n- **基础地址**:`https://api.deepseek.com`\\n- **认证方式**:API Key\\n- **模型列表**:deepseek-chat(最新模型)\\n- **端点**:`/v1/chat/completions`\\n- **支持功能**:上下文窗口、函数调用(Function Calling)、FIM 补全等\\n\\n### 4️⃣ **热门文档资源**\\n| 文档 | 说明 |\\n|------|------|\\n| 快速开始指南 | 新手接入教程 |\\n| API 参考 | 完整接口说明 |\\n| 模型对比 | 各版本能力对比 |\\n| 常见问题 | 使用中常见问题解答 |\\n\\n---\\n\\n如果您想了解 **某个具体的 DeepSeek 文档内容**(比如 API 调用方式、模型参数等),可以直接问我,我可以基于自身知识为您解答!有什么具体想了解的吗?😊', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 447, 'prompt_tokens': 562, 'total_tokens': 1009, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 69, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 178}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '4e5dc63e-a6f9-441a-98ff-f2c10f2277c5', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f3659-1c0b-72d3-b8f3-25264885e3dc-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 562, 'output_tokens': 447, 'total_tokens': 1009, 'input_token_details': {'cache_read': 384}, 'output_token_details': {'reasoning': 69}})]}\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", "\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=\"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", "builder.add_node(\"call_model\", call_model)\n", "builder.add_node(\"call_tools\", call_tools)\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", "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)\n" ] }, { "cell_type": "code", "execution_count": 14, "id": "fa8b5f2d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "=== 第一轮:查询天气 ===\n", "# 成都今日天气情况\n", "\n", "🌤 **天气状况**:局部多云(Partly cloudy)\n", "🌡 **温度**:**37°C**(🔥非常热!)\n", "💧 **湿度**:42%\n", "💨 **风速**:11 km/h\n", "🌫 **空气质量指数**:AQI **192(中度污染)**\n", "\n", "---\n", "\n", "## 适合跑步吗?❌ **不太建议**\n", "\n", "原因如下:\n", "\n", "1. **温度过高(37°C)**:这个温度下进行户外跑步,极易出现中暑、脱水等情况,对心脏和身体都是不小的负担。\n", "2. **空气质量中度污染(AQI 192)**:跑步时呼吸量会增大,吸入的污染物也会增多,对呼吸道和肺部健康不利。\n", "\n", "### 🙋‍♂️ 如果你想运动,建议:\n", "- ✅ **选择清晨或傍晚**温度稍低的时候\n", "- ✅ **改为室内运动**(健身房、游泳等)\n", "- ✅ 如果一定要出门跑步,请**降低强度、缩短时长**,并注意及时补充水分\n", "- ✅ 戴上运动口罩(但高温下也不太舒适)\n", "\n", "**总结:今天成都又热又有点脏,不太适合户外跑步,建议休息或室内运动更稳妥!** 🏃‍♂️💦\n", "\n", "=== 第二轮:追问空气质量 ===\n", "之前我已经查询过成都的空气质量数据,以下是详细情况:\n", "\n", "---\n", "\n", "## 🌫 成都空气质量\n", "\n", "| 指标 | 数值 |\n", "|------|:----:|\n", "| **AQI(空气质量指数)** | **192** |\n", "| **等级** | **中度污染** |\n", "| **主要影响** | 对敏感人群不友好 |\n", "\n", "### 等级说明\n", "- **0~50**:优 ✅\n", "- **51~100**:良 👍\n", "- **101~150**:轻度污染 ⚠️\n", "- **151~200**:中度污染 😷 ← **成都当前**\n", "- **201~300**:重度污染 ❌\n", "\n", "### 建议\n", "- 🚶 **敏感人群**(老人、儿童、呼吸道疾病患者)**减少户外活动**\n", "- 🏃 普通人群也建议**减少长时间户外剧烈运动**\n", "- 😷 如需外出,可考虑佩戴口罩\n", "- 室内建议开启**空气净化器**\n", "\n", "总体来说,成都今天的空气质量属于**中度污染**,不算理想,外出时稍加注意就好~\n", "\n", "=== 第三轮:测试记忆 ===\n", "您刚才问的正是——**成都** 🏙️\n", "\n", "我们聊了关于成都的:\n", "- ☀️ 天气:37°C,局部多云\n", "- 🌫 空气质量:AQI 192,中度污染\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", "\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": "05_langgraph (3.11.x)", "language": "python", "name": "python3" }, "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.-1" } }, "nbformat": 4, "nbformat_minor": 5 }